Repository: Red-Hat-AI-Innovation-Team/sdg_hub
Pull request #90: Minimal gui for flow creation
Author: login: akashgit
Labels: name: enhancement
description: New feature or request
Created: 2025-05-04T14:42:20Z | Merged: 2025-05-25T01:18:41Z

# Web Interface for SDG Hub

## Changes
- Added a web-based interface for building and managing SDG flows.
- Implemented drag-and-drop block creation and configuration.
- Block types are now discovered dynamically from the Python block registry (BlockRegistry) and their configuration schemas are generated from the block class's __init__ parameters.
- Added support for multiple block types (LLM, Filter, Iteration, etc.) (dynamically registered).
- Created YAML generation and parsing functionality.
- Updated main README.md (and web interface README) with dynamic block type discovery documentation.
- Updated requirements.txt with necessary dependencies.
- Force merged latest changes from main (commit 7d7cefc) to ensure minimal-gui is in sync.

## Features
- Drag-and-drop block creation.
- Visual block reordering (with up/down buttons).
- Block configuration (via a modal interface) – configuration fields are generated dynamically from the block's __init__ signature.
- YAML generation and parsing.
- Support for multiple block types (dynamically discovered from the registry).
- Save/load flows as JSON/YAML.

## Testing
- The web interface can be tested by running:
  ```bash
  cd web_interface
  python app.py
  ```
- Access the interface at http://127.0.0.1:8080.
- (Note: Block types are now discovered dynamically from the registry, so ensure that your block classes are registered (using @BlockRegistry.register) and that your __init__ parameters (with type hints) are as expected.)

## Screenshots
![Screenshot 2025-05-04 at 10 40 12 AM](https://github.com/user-attachments/assets/cdb562fb-d33a-43f7-9d07-c99d2dc69c9a)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a web-based drag-and-drop interface for building and managing SDG flows, including block creation, configuration, reordering, and YAML generation/parsing.
  - Added support for multiple block types with configurable parameters.
  - Enabled saving/loading flows as JSON or YAML.

- **Documentation**
  - Added comprehensive README detailing features, usage, development setup, file structure, and contribution guidelines.

- **Style**
  - Implemented new CSS styles for an interactive block-based UI.

- **Tests**
  - Added a script to verify block type configuration consistency.

- **Chores**
  - Added requirements files specifying necessary dependencies for both the main project and the web interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/requirements.txt b/requirements.txt
index 209f4cabc..1765d4eef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,5 @@ rich
 # https://github.com/instructlab/instructlab/issues/1389
 tenacity>=8.3.0,!=8.4.0
 tqdm>=4.66.2,<5.0.0
+flask==3.0.2
+pyyaml==6.0.1
diff --git a/web_interface/README.md b/web_interface/README.md
new file mode 100644
index 000000000..e5f57ff44
--- /dev/null
+++ b/web_interface/README.md
@@ -0,0 +1,134 @@
+# SDG Flow Builder Web Interface
+
+A web-based interface for building and managing SDG flows using a drag-and-drop interface.
+
+## Features
+
+- Drag-and-drop block creation
+- Visual block reordering with up/down buttons
+- Block configuration through a modal interface
+- YAML generation and parsing
+- Support for multiple block types (LLM, Filter, Iteration, etc.)
+
+## Block Types
+
+### LLM Block
+- Used for language model operations
+- Configurable model ID, output columns, and generation parameters
+- Supports batch processing and duplicate handling
+
+### Filter Block
+- Filters data based on column values
+- Supports various operations (equals, greater than, contains)
+- Can convert data types and drop columns
+
+### Iteration Block
+- Handles iterative operations
+- Configurable number of iterations
+- Specifies input field for iteration
+
+### Retrieval Model Block
+- Manages retrieval operations
+- Configurable model and query field
+- Supports top-k retrieval
+
+### Utility Block
+- General-purpose operations
+- Supports map, reduce, and filter operations
+- Configurable input field
+
+## Usage
+
+1. Start the Flask server:
+```bash
+cd web_interface
+python app.py
+```
+
+2. Access the interface at http://127.0.0.1:8080
+
+3. Build your flow:
+   - Drag blocks from the left panel to the canvas
+   - Use up/down buttons to reorder blocks
+   - Click the gear icon to configure block parameters
+   - Use the "Generate YAML" button to create YAML output
+
+4. Save and load flows:
+   - Use "Save Flow" to download the current flow as JSON
+   - Use "Load YAML" to import a YAML flow
+
+## Development
+
+The interface is built using:
+- Flask for the backend
+- Bootstrap for styling
+- Vanilla JavaScript for interactivity
+
+## File Structure
+
+- `app.py`: Main Flask application
+- `templates/index.html`: Main interface template
+- `static/`: Static assets (CSS, JS)
+- `requirements.txt`: Python dependencies
+
+## Contributing: Modifying Block Types
+
+The web interface now discovers block types **dynamically** from the Python block registry (`BlockRegistry`) and infers their configuration schemas from the `__init__` parameters of each block class. You **do not** need to manually edit a `BLOCK_TYPES` dictionary.
+
+### How Block Types Are Discovered
+- All block classes registered with `BlockRegistry` are automatically available in the web interface.
+- The configuration schema for each block is generated from the parameters of its `__init__` method (excluding `self` and `block_name`).
+- The parameter type hints and default values are used to build the schema shown in the UI.
+- The special parameter `gen_kwargs` is only included if it is present in the block's `__init__`.
+
+### Adding a New Block Type
+1. **Create a new block class** in Python, and decorate it with `@BlockRegistry.register()`:
+```python
+from sdg_hub.registry import BlockRegistry
+
+@BlockRegistry.register()
+class MyCustomBlock(Block):
+    def __init__(self, block_name: str, custom_param: str, optional_param: int = 0, gen_kwargs: dict = None):
+        super().__init__(block_name)
+        self.custom_param = custom_param
+        self.optional_param = optional_param
+        self.gen_kwargs = gen_kwargs or {}
+    # ... implement block logic ...
+```
+2. **Restart the Flask server**. Your new block will appear in the web interface, and its configuration form will be generated from the constructor signature.
+
+### Modifying an Existing Block Type
+- To add, remove, or change parameters, simply update the `__init__` method of the block class.
+- The web interface will automatically reflect these changes after a server restart.
+- To add `gen_kwargs` support, add it as a parameter to the constructor.
+
+### Removing a Block Type
+- Remove or comment out the block class, or unregister it from `BlockRegistry`.
+- The block will no longer appear in the web interface after a server restart.
+
+### Best Practices
+- Use type hints for all constructor parameters to ensure correct schema generation.
+- Provide default values for optional parameters.
+- Document your block classes and parameters in code.
+- Test your block in the web interface and with YAML generation/parsing.
+
+### Example: Adding a Data Transform Block
+```python
+from sdg_hub.registry import BlockRegistry
+
+@BlockRegistry.register()
+class TransformBlock(Block):
+    def __init__(self, block_name: str, transform_type: str, input_columns: list, output_columns: list, transform_params: dict = None):
+        super().__init__(block_name)
+        self.transform_type = transform_type
+        self.input_columns = input_columns
+        self.output_columns = output_columns
+        self.transform_params = transform_params or {}
+    # ... implement block logic ...
+```
+
+### After Making Changes
+1. Restart the Flask server to apply changes
+2. Test the new/modified block in the web interface
+3. Verify YAML generation and parsing
+4. Update any existing flows that might be affected 
\ No newline at end of file
diff --git a/web_interface/app.py b/web_interface/app.py
new file mode 100644
index 000000000..12243a26e
--- /dev/null
+++ b/web_interface/app.py
@@ -0,0 +1,227 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+from sdg_hub.registry import BlockRegistry
+import inspect
+from typing import get_type_hints, get_origin, get_args, Any
+from sdg_hub.blocks import *  # Ensure all blocks are registered
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+def get_block_types_from_registry():
+    """
+    Dynamically populate BLOCK_TYPES dictionary from the block registry.
+    Each block type will have a configuration schema based on its class's __init__ parameters.
+    """
+    block_types = {}
+    registry = BlockRegistry.get_registry()
+    
+    for block_name, block_class in registry.items():
+        if block_name.lower() == 'block':
+            continue  # Skip the base 'Block' type
+        # Get the __init__ method and its parameters
+        init_method = block_class.__init__
+        signature = inspect.signature(init_method)
+        type_hints = get_type_hints(init_method)
+        param_names = list(signature.parameters.keys())
+        # Start with base schema
+        config_schema = {
+            'block_name': {'type': 'string', 'required': True}
+        }
+        # Add parameters from __init__
+        for param_name, param in signature.parameters.items():
+            # Skip self, block_name, and client (already added or handled internally)
+            if param_name in ['self', 'block_name', 'client']:
+                continue
+            # Get parameter type and default
+            param_type = type_hints.get(param_name, Any)
+            param_default = param.default if param.default is not inspect.Parameter.empty else None
+            # Determine if parameter is required
+            is_required = param.default is inspect.Parameter.empty and param.kind not in [
+                inspect.Parameter.VAR_POSITIONAL,
+                inspect.Parameter.VAR_KEYWORD
+            ]
+            # Map Python types to schema types
+            type_mapping = {
+                str: 'string',
+                int: 'integer',
+                float: 'number',
+                bool: 'boolean',
+                list: 'array',
+                dict: 'object',
+                Any: 'string'  # Default to string for unknown types
+            }
+            # Handle special cases
+            if get_origin(param_type) is list:
+                schema_type = 'array'
+                items_type = get_args(param_type)[0] if get_args(param_type) else Any
+                schema = {
+                    'type': schema_type,
+                    'required': is_required,
+                    'items': {'type': type_mapping.get(items_type, 'string')}
+                }
+            elif get_origin(param_type) is dict:
+                schema_type = 'object'
+                schema = {
+                    'type': schema_type,
+                    'required': is_required,
+                    'properties': {}  # Could be expanded with specific dict properties
+                }
+            else:
+                schema_type = type_mapping.get(param_type, 'string')
+                schema = {
+                    'type': schema_type,
+                    'required': is_required
+                }
+            # Add default value if present
+            if param_default is not None:
+                schema['default'] = param_default
+            config_schema[param_name] = schema
+        # Only add gen_kwargs if it is present in __init__
+        if 'gen_kwargs' in param_names:
+            config_schema['gen_kwargs'] = {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'required': False, 'default': 0.7},
+                    'max_tokens': {'type': 'integer', 'required': False, 'default': 2048},
+                    'top_p': {'type': 'number', 'required': False, 'default': 1.0}
+                }
+            }
+        # Store the original block name as the key
+        block_types[block_name] = {
+            'name': block_name,
+            'config': config_schema
+        }
+    return block_types
+
+# Initialize BLOCK_TYPES from registry
+BLOCK_TYPES = get_block_types_from_registry()
+
+@app.route('/')
+def index():
+    """Render the main interface page with block types."""
+    try:
+        return render_template('index.html', block_types=BLOCK_TYPES)
+    except Exception as e:
+        print(f"Error rendering template: {str(e)}")
+        return str(e), 500
+
+@app.route('/api/blocks', methods=['GET'])
+def get_blocks():
+    return jsonify(BLOCK_TYPES)
+
+@app.route('/api/generate_yaml', methods=['POST'])
+def generate_yaml():
+    """
+    Generate YAML from the flow configuration.
+    
+    Converts the JSON flow data into the correct YAML format for SDG.
+    Each block is converted to the appropriate YAML structure with block_type and block_config.
+    """
+    try:
+        flow_data = request.json
+        if not flow_data:
+            return jsonify({'error': 'No data provided'}), 400
+        
+        # Convert the flow data to the correct YAML format
+        yaml_blocks = []
+        
+        # Process blocks in the order they appear in the flow
+        for block in flow_data['blocks']:
+            block_type = block['type']
+            block_config = block['config']
+            
+            # Create the block entry in the correct format
+            yaml_block = {
+                'block_type': block_type,
+                'block_config': {
+                    'block_name': block_config.get('block_name', f"{block_type}_{len(yaml_blocks)}"),
+                    **{k: v for k, v in block_config.items() if k != 'block_name'}
+                }
+            }
+            
+            # Add drop_duplicates if specified
+            if 'drop_duplicates' in block_config:
+                yaml_block['drop_duplicates'] = block_config['drop_duplicates']
+            
+            yaml_blocks.append(yaml_block)
+        
+        # Convert to YAML format
+        yaml_config = yaml.dump(yaml_blocks, default_flow_style=False)
+        return jsonify({'yaml': yaml_config})
+    except Exception as e:
+        print(f"Error generating YAML: {str(e)}")
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/api/parse_yaml', methods=['POST'])
+def parse_yaml():
+    """
+    Parse YAML into flow configuration.
+    
+    Converts YAML flow data into the format expected by the web interface.
+    Validates block types and creates appropriate block configurations.
+    """
+    try:
+        yaml_content = request.json.get('yaml', '')
+        if not yaml_content:
+            return jsonify({'error': 'No YAML content provided'}), 400
+            
+        yaml_blocks = yaml.safe_load(yaml_content)
+        
+        if not isinstance(yaml_blocks, list):
+            return jsonify({'error': 'Invalid YAML format. Expected a list of blocks.'}), 400
+        
+        # Convert YAML blocks to canvas format
+        blocks = []
+        
+        # First pass: create blocks
+        for i, yaml_block in enumerate(yaml_blocks):
+            block_type = yaml_block.get('block_type', '')
+            # Find the matching block type in BLOCK_TYPES (case-insensitive)
+            matching_block_type = next(
+                (bt for bt in BLOCK_TYPES.keys() if bt.lower() == block_type.lower()),
+                None
+            )
+            if not matching_block_type:
+                return jsonify({'error': f'Unknown block type: {block_type}'}), 400
+            
+            block_config = yaml_block.get('block_config', {})
+            
+            # Create block with position
+            block = {
+                'id': i + 1,  # Use index as ID
+                'type': matching_block_type,  # Use the original case from BLOCK_TYPES
+                'name': block_config.get('block_name', f"{matching_block_type}_{i}"),
+                'config': block_config,
+                'position': {
+                    'x': i * 250,  # Position blocks horizontally
+                    'y': 100
+                }
+            }
+            blocks.append(block)
+        
+        return jsonify({
+            'blocks': blocks,
+            'connections': [],
+            'firstBlockId': blocks[0]['id'] if blocks else None
+        })
+    
+    except yaml.YAMLError as e:
+        print(f"YAML parsing error: {str(e)}")
+        return jsonify({'error': f'Invalid YAML: {str(e)}'}), 400
+    except Exception as e:
+        print(f"Error parsing YAML: {str(e)}")
+        return jsonify({'error': f'Error parsing YAML: {str(e)}'}), 500
+
+if __name__ == '__main__':
+    app.run(debug=True, port=8080) 
\ No newline at end of file
diff --git a/web_interface/requirements.txt b/web_interface/requirements.txt
new file mode 100644
index 000000000..31863babc
--- /dev/null
+++ b/web_interface/requirements.txt
@@ -0,0 +1,3 @@
+flask==3.0.0
+pyyaml==6.0.1
+flask-wtf==1.2.1 
\ No newline at end of file
diff --git a/web_interface/static/css/style.css b/web_interface/static/css/style.css
new file mode 100644
index 000000000..f8a7c4e07
--- /dev/null
+++ b/web_interface/static/css/style.css
@@ -0,0 +1,171 @@
+.block-container {
+    min-height: 400px;
+    border: 2px dashed #ccc;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 20px;
+    position: relative;
+    background-color: #f8f9fa;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 20px;
+}
+
+.block-container.drag-over {
+    border-color: #0d6efd;
+    background-color: #e9ecef;
+}
+
+.block {
+    background: #ffffff;
+    border: 1px solid #dee2e6;
+    border-radius: 4px;
+    padding: 20px;
+    width: 300px;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+    transition: all 0.2s;
+    position: relative;
+    margin-bottom: 15px;
+    min-height: 100px;
+}
+
+.block:hover {
+    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
+}
+
+.block.selected {
+    border: 2px solid #0d6efd;
+    background-color: #f8f9fa;
+}
+
+.block.dragging {
+    opacity: 0.8;
+    transform: scale(1.05);
+    z-index: 1000;
+    box-shadow: 0 8px 16px rgba(0,0,0,0.2);
+}
+
+.block.drag-over {
+    transform: translateY(10px);
+}
+
+.block.drag-over-above {
+    transform: translateY(-10px);
+}
+
+.block-list {
+    max-height: 600px;
+    overflow-y: auto;
+}
+
+.block-list .block {
+    position: relative;
+    width: 100%;
+    margin-bottom: 10px;
+}
+
+.block-list .block:hover {
+    background: #e9ecef;
+}
+
+.block-list .block i {
+    color: #6c757d;
+}
+
+.block-order {
+    position: absolute;
+    left: -30px;
+    top: 50%;
+    transform: translateY(-50%);
+    background: #6c757d;
+    color: white;
+    padding: 2px 8px;
+    border-radius: 10px;
+    font-size: 12px;
+    z-index: 2;
+}
+
+.block.dragging .block-order {
+    background: #0d6efd;
+    transform: translateY(-50%) scale(1.1);
+}
+
+.drop-indicator {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: 4px;
+    background: #0d6efd;
+    border-radius: 2px;
+    z-index: 1000;
+    display: none;
+    transition: all 0.2s;
+}
+
+.drop-indicator.above {
+    top: -2px;
+}
+
+.drop-indicator.below {
+    bottom: -2px;
+}
+
+.drop-indicator.active {
+    height: 8px;
+    background: #198754;
+}
+
+.block-handle {
+    position: absolute;
+    left: 10px;
+    top: 20px;
+    color: #6c757d;
+    cursor: pointer;
+    transition: color 0.2s;
+}
+
+.block-handle:hover {
+    color: #0d6efd;
+}
+
+.block.dragging .block-handle {
+    color: #0d6efd;
+}
+
+.block-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 5px;
+    padding: 5px 0;
+}
+
+.block-actions button {
+    padding: 4px 10px;
+    font-size: 12px;
+}
+
+.block-content {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.block-name {
+    font-weight: 500;
+    margin-left: 30px;
+    font-size: 1.1em;
+    padding: 5px 0;
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+
+.block-type {
+    color: #6c757d;
+    font-size: 0.9em;
+}
+
+.block-name-value {
+    color: #212529;
+} 
\ No newline at end of file
diff --git a/web_interface/static/js/app.js b/web_interface/static/js/app.js
new file mode 100644
index 000000000..ab6b4ba20
--- /dev/null
+++ b/web_interface/static/js/app.js
@@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
+        
+        const formGroup = document.createElement('div');
+        formGroup.className = 'mb-3';
+        
+        const label = document.createElement('label');
+        label.className = 'form-label';
+        label.textContent = key;
+        if (config.required) {
+            label.innerHTML += ' <span class="text-danger">*</span>';
+        }
+        
+        let input;
+        if (config.type === 'number') {
+            input = document.createElement('input');
+            input.type = 'number';
+            input.className = 'form-control';
+            input.value = block.config[key] || config.default || '';
+        } else if (config.type === 'array') {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = (block.config[key] || []).join(', ');
+            input.placeholder = 'Comma-separated values';
+        } else if (config.type === 'object') {
+            input = document.createElement('textarea');
+            input.className = 'form-control';
+            input.value = JSON.stringify(block.config[key] || {}, null, 2);
+            input.rows = 3;
+            input.placeholder = 'JSON object';
+        } else if (config.enum) {
+            input = document.createElement('select');
+            input.className = 'form-select';
+            config.enum.forEach(option => {
+                const optionElement = document.createElement('option');
+                optionElement.value = option;
+                optionElement.textContent = option;
+                input.appendChild(optionElement);
+            });
+            input.value = block.config[key] || '';
+        } else {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = block.config[key] || '';
+        }
+        
+        input.name = key;
+        input.required = config.required;
+        
+        formGroup.appendChild(label);
+        formGroup.appendChild(input);
+        form.appendChild(formGroup);
+    });
+
+    blockConfigModal.show();
+}
+
+function saveBlockConfig() {
+    const form = document.getElementById('blockConfigForm');
+    const formData = new FormData(form);
+    const config = {};
+    
+    for (const [key, value] of formData.entries()) {
+        if (key === 'block_name') {
+            config[key] = value;
+        } else {
+            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
+            const fieldConfig = blockInfo.config[key];
+            
+            if (fieldConfig.type === 'array') {
+                config[key] = value.split(',').map(v => v.trim()).filter(v => v);
+            } else if (fieldConfig.type === 'object') {
+                try {
+                    config[key] = JSON.parse(value);
+                } catch (e) {
+                    config[key] = {};
+                }
+            } else if (fieldConfig.type === 'number') {
+                config[key] = parseFloat(value);
+            } else {
+                config[key] = value;
+            }
+        }
+    }
+    
+    const block = blocks.find(b => b.id === currentBlockId);
+    if (block) {
+        block.config = config;
+    }
+    
+    blockConfigModal.hide();
+    renderBlocks();
+}
+
+function saveFlow() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+    
+    // Generate YAML
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            throw new Error(data.error);
+        }
+        
+        // Create and download YAML file
+        const blob = new Blob([data.yaml], { type: 'text/yaml' });
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = 'flow.yaml';
+        document.body.appendChild(a);
+        a.click();
+        window.URL.revokeObjectURL(url);
+        document.body.removeChild(a);
+    })
+    .catch(error => {
+        console.error('Error saving flow:', error);
+        alert('Error saving flow: ' + error.message);
+    });
+}
+
+function generateYAML() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        const yamlOutput = document.getElementById('yaml-output');
+        yamlOutput.textContent = data.yaml;
+        yamlOutput.style.display = 'block';
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error generating YAML');
+    });
+}
+
+function loadYAML() {
+    yamlInputModal.show();
+}
+
+function parseYAML() {
+    const yamlContent = document.getElementById('yamlInput').value;
+    
+    fetch('/api/parse_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ yaml: yamlContent })
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            alert(data.error);
+            return;
+        }
+        
+        blocks = data.blocks;
+        renderBlocks();
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error parsing YAML');
+    });
+}
+
+function moveBlock(blockId, direction) {
+    const currentIndex = blocks.findIndex(b => b.id === blockId);
+    if (currentIndex === -1) return;
+    
+    let newIndex;
+    if (direction === 'up') {
+        newIndex = currentIndex - 1;
+    } else if (direction === 'down') {
+        newIndex = currentIndex + 1;
+    }
+    
+    // Check if the new index is valid
+    if (newIndex < 0 || newIndex >= blocks.length) {
+        return;
+    }
+    
+    // Swap blocks
+    [blocks[currentIndex], blocks[newIndex]] = [blocks[newIndex], blocks[currentIndex]];
+    renderBlocks();
+}
+
+function renderBlocks() {
+    flowCanvas.innerHTML = '';
+    
+    // Render blocks in their current order
+    blocks.forEach((block, index) => {
+        const blockElement = document.createElement('div');
+        blockElement.className = 'block';
+        blockElement.dataset.blockId = block.id;
+        blockElement.dataset.index = index;
+        
+        // Add drag handle
+        const dragHandle = document.createElement('div');
+        dragHandle.className = 'block-handle';
+        dragHandle.innerHTML = '<i class="fas fa-grip-vertical"></i>';
+        blockElement.appendChild(dragHandle);
+        
+        const blockContent = document.createElement('div');
+        blockContent.className = 'block-content';
+        
+        // Create name span
+        const nameSpan = document.createElement('span');
+        nameSpan.className = 'block-name';
+        
+        // Add block type
+        const typeSpan = document.createElement('span');
+        typeSpan.className = 'block-type';
+        typeSpan.textContent = block.type;
+        nameSpan.appendChild(typeSpan);
+        
+        // Add block name if it exists
+        if (block.config && block.config.block_name) {
+            const nameValueSpan = document.createElement('span');
+            nameValueSpan.className = 'block-name-value';
+            nameValueSpan.textContent = block.config.block_name;
+            nameSpan.appendChild(nameValueSpan);
+        }
+        
+        blockContent.appendChild(nameSpan);
+        
+        // Create buttons container
+        const buttonsContainer = document.createElement('div');
+        buttonsContainer.className = 'block-actions';
+        
+        // Create up button
+        const upButton = document.createElement('button');
+        upButton.className = 'btn btn-sm btn-outline-secondary';
+        upButton.innerHTML = '<i class="fas fa-arrow-up"></i>';
+        upButton.onclick = function() { moveBlock(block.id, 'up'); };
+        buttonsContainer.appendChild(upButton);
+        
+        // Create down button
+        const downButton = document.createElement('button');
+        downButton.className = 'btn btn-sm btn-outline-secondary';
+        downButton.innerHTML = '<i class="fas fa-arrow-down"></i>';
+        downButton.onclick = function() { moveBlock(block.id, 'down'); };
+        buttonsContainer.appendChild(downButton);
+        
+        // Create configure button
+        const configureButton = document.createElement('button');
+        configureButton.className = 'btn btn-sm btn-outline-primary';
+        configureButton.onclick = function() { configureBlock(block.id); };
+        configureButton.innerHTML = '<i class="fas fa-cog"></i>';
+        buttonsContainer.appendChild(configureButton);
+        
+        // Create remove button
+        const removeButton = document.createElement('button');
+        removeButton.className = 'btn btn-sm btn-outline-danger';
+        removeButton.onclick = function() { removeBlock(block.id); };
+        removeButton.innerHTML = '<i class="fas fa-times"></i>';
+        buttonsContainer.appendChild(removeButton);
+        
+        blockContent.appendChild(buttonsContainer);
+        blockElement.appendChild(blockContent);
+        flowCanvas.appendChild(blockElement);
+    });
+} 
\ No newline at end of file
diff --git a/web_interface/templates/index.html b/web_interface/templates/index.html
new file mode 100644
index 000000000..dabf50727
--- /dev/null
+++ b/web_interface/templates/index.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SDG Flow Builder</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
+    <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
+</head>
+<body>
+    <div class="container-fluid mt-4">
+        <h1 class="mb-4">SDG Flow Builder</h1>
+        
+        <div class="row">
+            <div class="col-md-3">
+                <div class="card">
+                    <div class="card-header">
+                        <h5 class="card-title mb-0">Available Blocks</h5>
+                    </div>
+                    <div class="card-body block-list">
+                        {% for block_id, block_info in block_types.items() %}
+                        <div class="block" draggable="true" data-block-type="{{ block_id }}">
+                            <div class="d-flex justify-content-between align-items-center">
+                                <span>{{ block_info.name }}</span>
+                                <i class="fas fa-grip-vertical"></i>
+                            </div>
+                        </div>
+                        {% endfor %}
+                    </div>
+                </div>
+            </div>
+            
+            <div class="col-md-9">
+                <div class="card">
+                    <div class="card-header d-flex justify-content-between align-items-center">
+                        <h5 class="card-title mb-0">Flow Canvas</h5>
+                        <div>
+                            <button class="btn btn-outline-primary me-2" onclick="saveFlow()">
+                                <i class="fas fa-save"></i> Save Flow
+                            </button>
+                            <button class="btn btn-outline-info me-2" onclick="loadYAML()">
+                                <i class="fas fa-file-code"></i> Load YAML
+                            </button>
+                            <button class="btn btn-primary" onclick="generateYAML()">
+                                <i class="fas fa-code"></i> Generate YAML
+                            </button>
+                        </div>
+                    </div>
+                    <div class="card-body">
+                        <div id="flow-canvas" class="block-container">
+                            <!-- Blocks will be dropped here -->
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="card mt-4">
+                    <div class="card-header">
+                        <h5 class="card-title mb-0">Generated YAML</h5>
+                    </div>
+                    <div class="card-body">
+                        <pre id="yaml-output" class="bg-light p-3" style="display: none;"></pre>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Block Configuration Modal -->
+    <div class="modal fade" id="blockConfigModal" tabindex="-1">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">Configure Block</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                </div>
+                <div class="modal-body">
+                    <form id="blockConfigForm">
+                        <!-- Dynamic form fields will be inserted here -->
+                    </form>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+                    <button type="button" class="btn btn-primary" onclick="saveBlockConfig()">Save</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- YAML Input Modal -->
+    <div class="modal fade" id="yamlInputModal" tabindex="-1">
+        <div class="modal-dialog modal-lg">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">Load YAML Flow</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <label for="yamlInput" class="form-label">Paste your YAML flow here:</label>
+                        <textarea id="yamlInput" class="form-control" rows="10"></textarea>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+                    <button type="button" class="btn btn-primary" onclick="parseYAML()">Load</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    <script>
+        // Pass block types to JavaScript
+        window.blockTypes = {{ block_types|tojson|safe }};
+    </script>
+    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/web_interface/test_block_types.py b/web_interface/test_block_types.py
new file mode 100644
index 000000000..fb4c88489
--- /dev/null
+++ b/web_interface/test_block_types.py
@@ -0,0 +1,75 @@
+"""
+Test script for get_block_types_from_registry function.
+This script will print out the block types and their configurations
+to verify that the schema is correctly populated from the block classes.
+It will also check that the config keys match the __init__ parameters of each block class.
+"""
+
+from app import get_block_types_from_registry
+from sdg_hub.blocks import BlockRegistry
+import inspect
+import json
+
+def test_block_types():
+    # Print the registry contents directly
+    print("\nDirect Registry Contents (from sdg_hub.blocks):")
+    print("=" * 80)
+    registry = BlockRegistry.get_registry()
+    print(f"Number of blocks in registry: {len(registry)}")
+    print("\nRegistered blocks:")
+    for name, block_class in registry.items():
+        print(f"- {name}: {block_class.__name__}")
+    
+    # Get block types from registry function
+    print("\nBlock Types from Registry Function:")
+    print("=" * 80)
+    block_types = get_block_types_from_registry()
+    print(f"\nNumber of block types: {len(block_types)}")
+    for block_name, block_info in block_types.items():
+        print(f"\nBlock Type: {block_name}")
+        print("-" * 40)
+        print(f"Name: {block_info['name']}")
+        print("\nConfiguration Schema:")
+        print(json.dumps(block_info['config'], indent=2))
+        print("=" * 80)
+
+    # Print the entire BLOCK_TYPES dict as pretty JSON
+    print("\nFull BLOCK_TYPES dictionary:")
+    print(json.dumps(block_types, indent=2))
+
+    # Get block types from registry function
+    block_types = get_block_types_from_registry()
+    registry = BlockRegistry.get_registry()
+
+    print("\n=== Block Properties Consistency Check ===\n")
+    all_ok = True
+    for block_type, block_info in block_types.items():
+        class_name = block_info['name']
+        block_class = registry[class_name]
+        # Get __init__ parameters (excluding self and block_name)
+        sig = inspect.signature(block_class.__init__)
+        param_names = [p for p in sig.parameters if p not in ('self', 'block_name')]
+        # Get config keys (excluding block_name, always present)
+        config_keys = [k for k in block_info['config'].keys() if k != 'block_name']
+        # Check for missing or extra keys
+        missing_in_config = [p for p in param_names if p not in config_keys]
+        extra_in_config = [k for k in config_keys if k not in param_names]
+        print(f"Block: {class_name}")
+        print(f"  __init__ params: {param_names}")
+        print(f"  config keys:    {config_keys}")
+        if not missing_in_config and not extra_in_config:
+            print("  ✅ Properties match!\n")
+        else:
+            all_ok = False
+            if missing_in_config:
+                print(f"  ❌ Missing in config: {missing_in_config}")
+            if extra_in_config:
+                print(f"  ❌ Extra in config:   {extra_in_config}")
+            print()
+    if all_ok:
+        print("All block config schemas match their class __init__ parameters!\n")
+    else:
+        print("Some blocks have mismatches between config and __init__ parameters.\n")
+
+if __name__ == "__main__":
+    test_block_types() 
\ No newline at end of file


Reviews:
user: login: sourcery-ai[bot]
body: Hey @akashgit - I've reviewed your changes - here's some feedback:

- Consider externalizing the `BLOCK_TYPES` definition from `app.py` into a separate configuration file (e.g., JSON or YAML) for easier modification.
- The frontend logic in `app.js` could be split into smaller, more focused modules to improve readability and maintainability.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 3 issues found
- 🟡 **Security**: 1 issue found
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T14:43:59Z
user: login: shivchander
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T20:16:25Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>♻️ Duplicate comments (6)</summary><blockquote>

<details>
<summary>web_interface/app.py (3)</summary><blockquote>

`14-14`: **Use environment variable for SECRET_KEY in production.**

The current random secret key will invalidate sessions on every restart. This is a duplicate of a previous review comment that should be addressed.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

14-14: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`18-95`: _🛠️ Refactor suggestion_

**Replace hardcoded block types with BlockRegistry.**

Based on previous review feedback, you should use the existing BlockRegistry instead of manually defining block types. Additionally, "rm" (Retrieval Model) and "util" (Utility) are flagged as invalid block types.



Replace the hardcoded `BLOCK_TYPES` with the registry:

```diff
+from sdg_hub.blocks import BlockRegistry

-# Define available block types and their configuration schemas
-# Each block type has a name, configuration options, and connection rules
-BLOCK_TYPES = {
-    'llm': {
-        # ... existing definition
-    },
-    # ... other block definitions
-}
+# Get available block types from registry
+def get_block_types():
+    registry = BlockRegistry.get_registry()
+    # Filter and format for web interface
+    block_types = {}
+    for name, block_class in registry.items():
+        if name != 'Block':  # Skip base class
+            # Extract configuration schema from block class
+            block_types[name.lower().replace('block', '')] = {
+                'name': name,
+                'config': extract_config_schema(block_class),
+                'connections': extract_connections(block_class)
+            }
+    return block_types
+
+BLOCK_TYPES = get_block_types()
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

19-19: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

20-20: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

20-20: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

21-21: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

26-26: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

27-27: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

27-27: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

28-28: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

37-37: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

42-42: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

44-44: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

71-71: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

71-71: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

72-72: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

78-78: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

84-84: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

84-84: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

85-85: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`103-104`: _🛠️ Refactor suggestion_

**Replace print statements with proper logging.**

This duplicates previous review feedback about using a logging framework instead of print statements for better error tracking.



Apply this diff to use Flask's logger:

```diff
-        print(f"Error rendering template: {str(e)}")
+        app.logger.error("Error rendering template: %s", str(e))
-        print(f"Error generating YAML: {str(e)}")
+        app.logger.error("Error generating YAML: %s", str(e))
-        print(f"YAML parsing error: {str(e)}")
+        app.logger.error("YAML parsing error: %s", str(e))
-        print(f"Error parsing YAML: {str(e)}")
+        app.logger.error("Error parsing YAML: %s", str(e))
```


Also applies to: 150-151, 202-203, 205-206

</blockquote></details>
<details>
<summary>web_interface/static/js/app.js (3)</summary><blockquote>

`298-299`: _⚠️ Potential issue_

**Critical: Guard against missing block in config save.**

This is similar to a past review comment but the issue still exists. The code doesn't check if `blocks.find()` returns undefined before accessing the `.type` property.



Apply this fix:

```diff
-            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
+            const block = blocks.find(b => b.id === currentBlockId);
+            if (!block) {
+                console.warn('Block not found for id:', currentBlockId);
+                continue;
+            }
+            const blockInfo = blockTypes[block.type];
```

---

`326-363`: _🛠️ Refactor suggestion_

**Extract duplicate YAML generation logic into shared function.**

This addresses a past review comment about abstracting duplicate YAML generation logic. Both `saveFlow` and `generateYAML` contain identical fetch logic.



Create a shared helper function:

```diff
+function fetchYAMLFromServer(flowData) {
+    return fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            throw new Error(data.error);
+        }
+        return data.yaml;
+    });
+}
+
 function saveFlow() {
     const flowData = {
         blocks: blocks.map(block => ({
@@ -332,32 +349,18 @@
         }))
     };
     
-    // Generate YAML
-    fetch('/api/generate_yaml', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json'
-        },
-        body: JSON.stringify(flowData)
-    })
-    .then(response => response.json())
-    .then(data => {
-        if (data.error) {
-            throw new Error(data.error);
-        }
-        
+    fetchYAMLFromServer(flowData)
+    .then(yaml => {
         // Create and download YAML file
-        const blob = new Blob([data.yaml], { type: 'text/yaml' });
+        const blob = new Blob([yaml], { type: 'text/yaml' });
         const url = window.URL.createObjectURL(blob);
         const a = document.createElement('a');
         a.href = url;
```

---

`365-390`: _🛠️ Refactor suggestion_

**Apply the same refactoring to generateYAML function.**

Continuing the refactoring to use the shared helper function:


```diff
 function generateYAML() {
     const flowData = {
         blocks: blocks.map(block => ({
@@ -370,22 +372,12 @@
         }))
     };

-    fetch('/api/generate_yaml', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json'
-        },
-        body: JSON.stringify(flowData)
-    })
-    .then(response => response.json())
-    .then(data => {
+    fetchYAMLFromServer(flowData)
+    .then(yaml => {
         const yamlOutput = document.getElementById('yaml-output');
-        yamlOutput.textContent = data.yaml;
+        yamlOutput.textContent = yaml;
         yamlOutput.style.display = 'block';
     })
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>web_interface/static/js/app.js (2)</summary><blockquote>

`60-60`: **Consider using crypto.randomUUID() for more robust ID generation.**

Using `Date.now()` for block IDs could lead to collisions if multiple blocks are created rapidly. Consider using a more robust ID generation method.


```diff
-        id: Date.now(),
+        id: crypto.randomUUID(),
```

---

`473-473`: **Use optional chaining for safer property access.**

The static analysis tool correctly identifies that optional chaining would be safer here.


```diff
-        if (block.config && block.config.block_name) {
+        if (block.config?.block_name) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 473-473: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>
<details>
<summary>web_interface/static/css/style.css (2)</summary><blockquote>

`76-92`: **Consider responsive positioning for block order indicators.**

The fixed left positioning (`left: -30px`) might cause issues on smaller screens or within constrained containers.


Consider adding responsive adjustments:

```diff
 .block-order {
     position: absolute;
-    left: -30px;
+    left: -30px;
     top: 50%;
     transform: translateY(-50%);
     background: #6c757d;
     color: white;
     padding: 2px 8px;
     border-radius: 10px;
     font-size: 12px;
     z-index: 2;
 }
+
+@media (max-width: 768px) {
+    .block-order {
+        left: -20px;
+        padding: 1px 6px;
+        font-size: 10px;
+    }
+}
```

---

`20-31`: **Add responsive width for better mobile experience.**

The fixed width of `300px` for blocks might not work well on smaller screens.


```diff
 .block {
     background: #ffffff;
     border: 1px solid #dee2e6;
     border-radius: 4px;
     padding: 20px;
-    width: 300px;
+    width: 300px;
+    max-width: 100%;
     box-shadow: 0 2px 4px rgba(0,0,0,0.1);
     transition: all 0.2s;
     position: relative;
     margin-bottom: 15px;
     min-height: 100px;
 }
+
+@media (max-width: 576px) {
+    .block {
+        width: 100%;
+        padding: 15px;
+    }
+}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1ee933534c467fe9004019803e4596d6efb7888a and ed2b185a4ad78344968da9dcc45c487747b3bcda.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `requirements.txt` (1 hunks)
* `web_interface/README.md` (1 hunks)
* `web_interface/app.py` (1 hunks)
* `web_interface/requirements.txt` (1 hunks)
* `web_interface/static/css/style.css` (1 hunks)
* `web_interface/static/js/app.js` (1 hunks)
* `web_interface/templates/index.html` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>web_interface/static/js/app.js (1)</summary><blockquote>

<details>
<summary>web_interface/app.py (1)</summary>

* `index` (98-104)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>web_interface/README.md</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>web_interface/static/js/app.js</summary>

[error] 473-473: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>
<details>
<summary>🪛 Ruff (0.11.9)</summary>

<details>
<summary>web_interface/app.py</summary>

14-14: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

19-19: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

20-20: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

20-20: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

21-21: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

23-23: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

24-24: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

25-25: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

26-26: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

27-27: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

27-27: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

28-28: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

30-30: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

31-31: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

34-34: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

35-35: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

37-37: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

39-39: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

42-42: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

44-44: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

45-45: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

46-46: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

48-48: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

49-49: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

50-50: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

54-54: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

62-62: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

63-63: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

66-66: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

67-67: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

71-71: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

71-71: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

72-72: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

74-74: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

78-78: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

79-79: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

84-84: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

84-84: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

85-85: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

86-86: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

88-88: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

101-101: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

106-106: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

106-106: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

110-110: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

110-110: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

121-121: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

121-121: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

127-127: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

128-128: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

129-129: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

134-134: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

135-135: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

135-135: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

136-136: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

141-141: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

142-142: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

142-142: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

151-151: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

153-153: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

153-153: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

162-162: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

162-162: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

164-164: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

164-164: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

169-169: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

169-169: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

178-178: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

178-178: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

180-180: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

184-184: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

185-185: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

186-186: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

186-186: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

187-187: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

189-189: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

190-190: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

196-196: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

197-197: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

203-203: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

203-203: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

206-206: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

206-206: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

208-208: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>
<details>
<summary>🪛 ast-grep (0.38.1)</summary>

<details>
<summary>web_interface/app.py</summary>

[warning] 208-208: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True, port=8080)
Note: [CWE-489] Active Debug Code. [REFERENCES]
    - https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: 3.11 on ubuntu-latest
* GitHub Check: 3.10 on ubuntu-latest

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>requirements.txt (1)</summary>

`15-16`: <details>
<summary>✅ Verification successful</summary>

**Verify Flask and PyYAML versions are current and secure.**

The pinned versions should be verified for security vulnerabilities and compatibility with the existing dependency stack.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for latest versions and security advisories
echo "Checking Flask version..."
curl -s https://pypi.org/pypi/flask/json | jq '.info.version'

echo "Checking PyYAML version..."  
curl -s https://pypi.org/pypi/pyyaml/json | jq '.info.version'

echo "Checking for security advisories..."
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: PIP, package: "flask") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
    }
  }
}' 

gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: PIP, package: "pyyaml") {
    nodes {
      advisory {
        summary
        severity
        publishedAt  
      }
      vulnerableVersionRange
    }
  }
}'
```

Length of output: 2621

---



</details>

**Flask and PyYAML pins are secure; consider upgrading to the latest patch versions**

Verified via PyPI and the GitHub Advisory API that:
- flask==3.0.2 and pyyaml==6.0.1 have no known PIP security advisories.
- The current patch releases on PyPI are flask==3.1.1 and pyyaml==6.0.2.

To pick up recent bug-fixes, you can update your requirements.txt:

```diff
-flask==3.0.2
-pyyaml==6.0.1
+flask==3.1.1
+pyyaml==6.0.2
```

</details>
<details>
<summary>web_interface/README.md (1)</summary>

`1-72`: **Excellent documentation for the web interface!**

The README provides comprehensive coverage of features, block types, usage instructions, and development setup. This will help users understand and maintain the web interface effectively.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

</details>
<details>
<summary>web_interface/templates/index.html (1)</summary>

`1-119`: **Well-structured HTML template with good UI design.**

The template effectively implements:
- Responsive Bootstrap 5 layout
- Drag-and-drop interface with clear visual hierarchy  
- Modal dialogs for block configuration and YAML input
- Proper JavaScript integration with backend data
- Clean separation of concerns between UI elements

The implementation supports the drag-and-drop workflow described in the PR objectives.

</details>
<details>
<summary>web_interface/static/js/app.js (1)</summary>

`1-10`: **LGTM - Good initialization and global state management.**

The global variables are well-organized and the Bootstrap modal initialization is appropriate for this use case.

</details>
<details>
<summary>web_interface/static/css/style.css (1)</summary>

`1-171`: **Excellent CSS organization and comprehensive styling.**

The CSS is well-organized with clear class naming conventions, appropriate use of transitions, and comprehensive coverage of drag-and-drop states. The visual feedback for user interactions is intuitive and professional.

Key strengths:
- Consistent naming convention (BEM-like approach)
- Smooth transitions for better UX
- Comprehensive drag-and-drop state styling
- Good use of z-index for layering
- Appropriate color scheme with Bootstrap compatibility

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T20:47:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>web_interface/app.py (1)</summary><blockquote>

`14-14`: **Consider avoiding wildcard imports for better code maintainability.**

The wildcard import `from sdg_hub.blocks import *` can make it difficult to track dependencies and may introduce unexpected names into the namespace.


Consider being more explicit about imports:

```diff
-from sdg_hub.blocks import *  # Ensure all blocks are registered
+# Import specific block classes or use a module-level import
+import sdg_hub.blocks  # Ensure all blocks are registered
```

This change maintains the registration side effect while improving code clarity.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ed2b185a4ad78344968da9dcc45c487747b3bcda and 5db991e5b36a1e1bfa2b5aaf416dab92c750ede1.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web_interface/README.md` (1 hunks)
* `web_interface/app.py` (1 hunks)
* `web_interface/test_block_types.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>web_interface/test_block_types.py (2)</summary><blockquote>

<details>
<summary>web_interface/app.py (1)</summary>

* `get_block_types_from_registry` (20-104)

</details>
<details>
<summary>src/sdg_hub/registry.py (1)</summary>

* `BlockRegistry` (13-43)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.11.9)</summary>

<details>
<summary>web_interface/test_block_types.py</summary>

33-33: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>
<details>
<summary>web_interface/app.py</summary>

18-18: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

56-56: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

57-57: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

68-68: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

69-69: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

77-77: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

82-82: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

93-93: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

94-94: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

101-101: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

102-102: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

109-109: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

113-113: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

118-118: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

118-118: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

122-122: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

122-122: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

139-139: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

140-140: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

141-141: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

145-145: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

146-146: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

153-153: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

154-154: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

154-154: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

160-160: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

163-163: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

165-165: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

165-165: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

174-174: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

174-174: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

181-181: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

181-181: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

190-190: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

190-190: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

192-192: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

196-196: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

197-197: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

199-199: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

200-200: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

201-201: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

202-202: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

208-208: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

209-209: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

210-210: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

210-210: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

215-215: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

215-215: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

218-218: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

218-218: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

220-220: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>web_interface/README.md</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>
<details>
<summary>🪛 ast-grep (0.38.1)</summary>

<details>
<summary>web_interface/app.py</summary>

[warning] 220-220: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True, port=8080)
Note: [CWE-489] Active Debug Code. [REFERENCES]
    - https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: 3.10 on ubuntu-latest
* GitHub Check: 3.11 on ubuntu-latest

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>web_interface/test_block_types.py (2)</summary>

`8-11`: **LGTM: Well-structured test imports and dependencies.**

The imports are clean and appropriate for the testing purpose. Good use of the registry pattern and inspection utilities.

---

`13-75`: **Excellent test implementation for block type consistency.**

This test script provides valuable verification by:
- Comparing block class `__init__` parameters with generated config schemas
- Providing detailed output for debugging schema mismatches
- Ensuring consistency between backend block definitions and frontend configurations

The logic is sound and the output format is user-friendly for identifying discrepancies.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

33-33: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

47-47: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

51-51: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

53-53: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

</details>
<details>
<summary>web_interface/README.md (1)</summary>

`1-186`: **Excellent comprehensive documentation for the web interface.**

This README provides outstanding documentation with:
- Clear feature descriptions and usage instructions
- Detailed block type explanations
- Comprehensive development and contribution guidelines
- Code examples and best practices
- Well-structured sections for easy navigation

The contributing section (lines 74-186) is particularly valuable for maintainability, providing detailed guidance on modifying block types with examples and best practices.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

</details>
<details>
<summary>web_interface/app.py (3)</summary>

`20-105`: **Excellent dynamic block type generation implementation.**

The `get_block_types_from_registry()` function demonstrates sophisticated introspection:
- Properly handles parameter types, defaults, and requirements
- Supports complex types (lists, dicts) with appropriate schema generation
- Includes special handling for `gen_kwargs` parameter
- Maps Python types to schema types correctly

This dynamic approach eliminates the need for manual block type definitions and ensures consistency between block classes and their configurations.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

56-56: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

57-57: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

68-68: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

69-69: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

77-77: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

82-82: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

93-93: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

94-94: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

101-101: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

102-102: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`122-164`: **YAML generation logic is well-implemented with proper error handling.**

The `/api/generate_yaml` endpoint correctly:
- Converts frontend block configurations to YAML format
- Handles the `block_type` formatting with proper casing
- Preserves optional fields like `drop_duplicates`
- Includes comprehensive error handling

The transformation from frontend JSON to SDG YAML format is accurate and maintains data integrity.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

122-122: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

122-122: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

139-139: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

140-140: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

141-141: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

145-145: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

146-146: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

153-153: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

154-154: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

154-154: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

160-160: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

163-163: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`165-219`: **Robust YAML parsing with proper validation.**

The `/api/parse_yaml` endpoint provides excellent functionality:
- Validates YAML structure and block types against the registry
- Converts YAML blocks to the frontend canvas format
- Handles positioning for visual layout
- Includes proper error handling for both YAML parsing and validation errors

The block type validation on line 189-190 ensures only known block types are processed, preventing invalid configurations.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

165-165: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

165-165: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

174-174: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

174-174: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

176-176: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

181-181: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

181-181: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

188-188: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

190-190: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

190-190: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

192-192: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

196-196: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

197-197: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

199-199: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

200-200: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

201-201: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

202-202: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

208-208: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

209-209: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

210-210: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

210-210: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

215-215: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

215-215: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

218-218: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

218-218: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T21:56:07Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>web_interface/README.md (4)</summary><blockquote>

`1-4`: **Consider adding a Table of Contents.**
For improved navigation in this lengthy README, it would help to introduce a linked TOC listing key sections like Features, Usage, Development, Contributing, etc., so readers can quickly jump to the information they need.

---

`15-19`: **Standardize Block Types formatting.**
To enhance readability and make block type names stand out, consider wrapping them in inline code formatting (e.g., `LLM Block`, `Filter Block`) or using a consistent heading level/style for each block type section.

---

`40-49`: **Specify supported Python version.**
It would be beneficial to clarify the required Python version (for example, Python 3.8+) in the Usage or a new Prerequisites section to set clear environment expectations.

---

`67-73`: **Include testing instructions in File Structure.**
Under File Structure, list the `test_block_types.py` script and add a note on how to run it, e.g.:
```bash
python -m unittest web_interface/test_block_types.py
```
This will improve onboarding for contributors who want to verify block type schemas.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5db991e5b36a1e1bfa2b5aaf416dab92c750ede1 and 69d1adb89159954f7edc23efd82fbb90b049ba24.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `web_interface/README.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>web_interface/README.md</summary>

[uncategorized] ~69-~69: Loose punctuation mark.
Context: ...ractivity  ## File Structure  - `app.py`: Main Flask application - `templates/ind...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: 3.10 on ubuntu-latest
* GitHub Check: 3.11 on macos-latest
* GitHub Check: 3.11 on ubuntu-latest

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T21:59:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>web_interface/app.py (3)</summary><blockquote>

`116-116`: **Inconsistent error handling with print statements.**

The application uses print statements for error logging instead of Python's logging framework. This makes it difficult to control log levels, format messages consistently, or redirect logs in production environments.



This issue was previously flagged in the existing comments suggesting the use of `app.logger.error()` instead of print statements for better error reporting and traceability.


Also applies to: 163-163, 220-220, 223-223

---

`145-151`: **YAML generation logic assumes exact block_type match.**

The code directly uses `block_type` from the frontend without validation, which could lead to invalid YAML if the frontend sends an incorrect or modified block type name.



This issue was previously flagged in the existing comments suggesting validation of block_type against the BLOCK_TYPES registry before generating YAML.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

146-146: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

149-149: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`226-227`: **Debug mode enabled in production.**



This security issue was already flagged in the existing comments suggesting environment-based configuration to disable debug mode for production deployment.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

226-226: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>
<details>
<summary>🪛 ast-grep (0.38.1)</summary>

[warning] 226-226: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True, port=8080)
Note: [CWE-489] Active Debug Code. [REFERENCES]
    - https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>web_interface/app.py (4)</summary><blockquote>

`20-108`: **Consider refactoring the complex block discovery function.**

The `get_block_types_from_registry()` function is quite large (88 lines) and handles multiple responsibilities including type introspection, schema generation, and special case handling. This violates the Single Responsibility Principle and makes the code harder to maintain and test.



Consider breaking this function into smaller, focused functions:

```python
def get_block_types_from_registry():
    """Main function to get block types from registry."""
    block_types = {}
    registry = BlockRegistry.get_registry()
    
    for block_name, block_class in registry.items():
        if block_name.lower() == "block":
            continue
        
        config_schema = _generate_config_schema(block_class)
        block_types[block_name] = {
            "name": block_name,
            "config": config_schema
        }
    return block_types

def _generate_config_schema(block_class):
    """Generate configuration schema for a block class."""
    # Extract current logic for schema generation
    pass

def _get_python_type_mapping():
    """Get mapping from Python types to schema types."""
    # Extract type mapping logic
    pass

def _handle_special_parameter_types(param_type, is_required):
    """Handle special cases like list, dict, etc."""
    # Extract special type handling logic
    pass
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

56-56: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

57-57: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

68-68: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

69-69: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

77-77: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

82-82: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

93-93: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

94-94: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

102-102: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

103-103: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`43-43`: **Consider making the excluded parameters configurable.**

The hardcoded list of excluded parameters (`'self', 'block_name', 'client'`) might need to be updated if new common parameters are added to block classes in the future.



Consider making this configurable or moving it to a constant:

```diff
+# Parameters to exclude from dynamic schema generation
+EXCLUDED_INIT_PARAMS = {'self', 'block_name', 'client'}

def get_block_types_from_registry():
    # ... existing code ...
    for param_name, param in signature.parameters.items():
-        if param_name in ['self', 'block_name', 'client']:
+        if param_name in EXCLUDED_INIT_PARAMS:
            continue
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`61-61`: **Handle unknown types more robustly.**

Defaulting unknown types to 'string' might not always be appropriate and could lead to validation issues in the frontend. Consider providing better type inference or validation.



```diff
-                Any: 'string'  # Default to string for unknown types
+                Any: 'string',  # Default fallback
+                type(None): 'string'  # Handle None type
            }
+            
+            # Handle unknown types more gracefully
+            if param_type not in type_mapping and not get_origin(param_type):
+                # Log warning for unknown types
+                print(f"Warning: Unknown type {param_type} for parameter {param_name} in {block_class.__name__}")
+                schema_type = 'string'  # Safe fallback
+            else:
                schema_type = type_mapping.get(param_type, 'string')
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`94-99`: **Hardcoded gen_kwargs properties may become outdated.**

The special handling for `gen_kwargs` includes hardcoded properties like 'temperature', 'max_tokens', and 'top_p' with default values. These might become outdated as the underlying libraries evolve.



Consider making these properties configurable or discovering them dynamically:

```python
# Configuration for common LLM generation parameters
GEN_KWARGS_SCHEMA = {
    'temperature': {'type': 'number', 'required': False, 'default': 0.7},
    'max_tokens': {'type': 'integer', 'required': False, 'default': 2048},
    'top_p': {'type': 'number', 'required': False, 'default': 1.0},
    # Add more as needed
}

# In the function:
if 'gen_kwargs' in param_names:
    config_schema['gen_kwargs'] = {
        'type': 'object',
        'required': False,
        'properties': GEN_KWARGS_SCHEMA
    }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

94-94: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 69d1adb89159954f7edc23efd82fbb90b049ba24 and 9c3833fddd89499fa435a8b019e12a42b873a54c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `web_interface/app.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

<details>
<summary>web_interface/app.py</summary>

18-18: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

29-29: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

38-38: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

43-43: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

55-55: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

56-56: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

57-57: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

58-58: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

59-59: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

60-60: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

61-61: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

65-65: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

68-68: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

69-69: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

70-70: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

73-73: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

75-75: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

76-76: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

77-77: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

80-80: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

82-82: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

83-83: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

87-87: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

90-90: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

91-91: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

92-92: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

93-93: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

94-94: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

95-95: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

96-96: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

97-97: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

102-102: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

103-103: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

110-110: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

114-114: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

119-119: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

119-119: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

123-123: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

123-123: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

134-134: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

134-134: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

140-140: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

141-141: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

142-142: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

146-146: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

147-147: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

148-148: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

149-149: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

154-154: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

155-155: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

155-155: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

161-161: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

164-164: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

166-166: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

166-166: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

175-175: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

175-175: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

177-177: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

177-177: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

182-182: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

182-182: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

189-189: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

189-189: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

196-196: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

196-196: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

198-198: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

202-202: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

203-203: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

204-204: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

204-204: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

205-205: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

206-206: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

207-207: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

208-208: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

214-214: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

215-215: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

216-216: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

216-216: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

221-221: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

221-221: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

224-224: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

224-224: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

226-226: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>
<details>
<summary>🪛 ast-grep (0.38.1)</summary>

<details>
<summary>web_interface/app.py</summary>

[warning] 226-226: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True, port=8080)
Note: [CWE-489] Active Debug Code. [REFERENCES]
    - https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: 3.11 on ubuntu-latest
* GitHub Check: 3.10 on ubuntu-latest

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T01:03:08Z
user: login: shivchander
body: Approving the PR for now to unblock progress. That said, we’ll need a follow-up to properly fix the type handling—fields that are supposed to accept lists are currently being read as plain strings.

I’ll handle updating the block type definitions so that these inputs are correctly parsed and validated going forward.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-25T01:12:19Z

Review comments:
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
+        
+        const formGroup = document.createElement('div');
+        formGroup.className = 'mb-3';
+        
+        const label = document.createElement('label');
+        label.className = 'form-label';
+        label.textContent = key;
+        if (config.required) {
+            label.innerHTML += ' <span class="text-danger">*</span>';
+        }
+        
+        let input;
+        if (config.type === 'number') {
+            input = document.createElement('input');
+            input.type = 'number';
+            input.className = 'form-control';
+            input.value = block.config[key] || config.default || '';
+        } else if (config.type === 'array') {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = (block.config[key] || []).join(', ');
+            input.placeholder = 'Comma-separated values';
+        } else if (config.type === 'object') {
+            input = document.createElement('textarea');
+            input.className = 'form-control';
+            input.value = JSON.stringify(block.config[key] || {}, null, 2);
+            input.rows = 3;
+            input.placeholder = 'JSON object';
+        } else if (config.enum) {
+            input = document.createElement('select');
+            input.className = 'form-select';
+            config.enum.forEach(option => {
+                const optionElement = document.createElement('option');
+                optionElement.value = option;
+                optionElement.textContent = option;
+                input.appendChild(optionElement);
+            });
+            input.value = block.config[key] || '';
+        } else {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = block.config[key] || '';
+        }
+        
+        input.name = key;
+        input.required = config.required;
+        
+        formGroup.appendChild(label);
+        formGroup.appendChild(input);
+        form.appendChild(formGroup);
+    });
+
+    blockConfigModal.show();
+}
+
+function saveBlockConfig() {
+    const form = document.getElementById('blockConfigForm');
+    const formData = new FormData(form);
+    const config = {};
+    
+    for (const [key, value] of formData.entries()) {
+        if (key === 'block_name') {
+            config[key] = value;
+        } else {
+            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Guard against a missing block in the config save.

Ensure `blocks.find(...)` returns a block before accessing its `.type` to avoid runtime errors.

Suggested implementation:

```javascript
            const block = blocks.find(b => b.id === currentBlockId);
            if (!block) {
                console.warn('Block not found for id:', currentBlockId);
                continue;
            }
            const blockInfo = blockTypes[block.type];

```

These changes ensure that if blocks.find returns undefined, the code logs a warning and skips processing for that configuration entry, avoiding a runtime error.
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
path: web_interface/app.py
user: login: sourcery-ai[bot]
body: **🚨 suggestion (security):** Review secret key management for production.

While os.urandom is fine for development, use a persistent secret key in production to avoid session invalidation.

```suggestion
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24))  # Use persistent secret key from environment variable in production, fallback to random key for development
```
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
+    'llm': {
+        'name': 'LLM Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'config_path': {'type': 'string', 'required': True},
+            'model_id': {'type': 'string', 'required': True},
+            'output_cols': {'type': 'array', 'required': True},
+            'gen_kwargs': {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'default': 0.7},
+                    'max_tokens': {'type': 'number', 'default': 2048}
+                }
+            },
+            'drop_duplicates': {'type': 'array', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'filter': {
+        'name': 'Filter Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'filter_column': {'type': 'string', 'required': True},
+            'filter_value': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True},
+            'convert_dtype': {'type': 'string', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False},
+            'drop_columns': {'type': 'array', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'iter': {
+        'name': 'Iteration Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'iterations': {'type': 'number', 'required': True},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'rm': {
+        'name': 'Retrieval Model Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'model': {'type': 'string', 'required': True},
+            'query_field': {'type': 'string', 'required': True},
+            'top_k': {'type': 'number', 'required': False, 'default': 5}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'util': {
+        'name': 'Utility Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True, 'enum': ['map', 'reduce', 'filter']},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    }
+}
+
+@app.route('/')
+def index():
+    """Render the main interface page with block types."""
+    try:
+        return render_template('index.html', block_types=BLOCK_TYPES)
+    except Exception as e:
+        print(f"Error rendering template: {str(e)}")
+        return str(e), 500
+
+@app.route('/api/blocks', methods=['GET'])
+def get_blocks():
+    return jsonify(BLOCK_TYPES)
+
+@app.route('/api/generate_yaml', methods=['POST'])
+def generate_yaml():
+    """
+    Generate YAML from the flow configuration.
+    
+    Converts the JSON flow data into the correct YAML format for SDG.
+    Each block is converted to the appropriate YAML structure with block_type and block_config.
+    """
+    try:
+        flow_data = request.json
+        if not flow_data:
+            return jsonify({'error': 'No data provided'}), 400
+        
+        # Convert the flow data to the correct YAML format
+        yaml_blocks = []
+        
+        # Process blocks in the order they appear in the flow
+        for block in flow_data['blocks']:
+            block_type = block['type']
+            block_config = block['config']
+            
+            # Create the block entry in the correct format
+            yaml_block = {
+                'block_type': f"{block_type.upper()}Block",
+                'block_config': {
+                    'block_name': block_config.get('block_name', f"{block_type}_{len(yaml_blocks)}"),
+                    **{k: v for k, v in block_config.items() if k != 'block_name'}
+                }
+            }
+            
+            # Add drop_duplicates if specified
+            if 'drop_duplicates' in block_config:
+                yaml_block['drop_duplicates'] = block_config['drop_duplicates']
+            
+            yaml_blocks.append(yaml_block)
+        
+        # Convert to YAML format
+        yaml_config = yaml.dump(yaml_blocks, default_flow_style=False)
+        return jsonify({'yaml': yaml_config})
+    except Exception as e:
+        print(f"Error generating YAML: {str(e)}")
+        return jsonify({'error': str(e)}), 500
path: web_interface/app.py
user: login: sourcery-ai[bot]
body: **suggestion:** Consider using a logging framework for error reporting.

Structured logging offers better traceability and control than print statements, especially for production debugging.

```suggestion
    except Exception as e:
        app.logger.error("Error generating YAML: %s", str(e))
        return jsonify({'error': str(e)}), 500
```
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion:** Abstract duplicate YAML generation logic.

Move the duplicate fetch logic in saveFlow and generateYAML into a shared helper to reduce code duplication and improve maintainability.
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting common logic for DOM manipulation and drag-and-drop functionality into helper functions to reduce code duplication and nesting.

Consider extracting and consolidating common patterns in the drag‐and‐drop and dynamic DOM manipulation logic to reduce repetition and nesting. For example, you can create helper functions to update drop indicators and to generate common form groups. For instance:

```js
// Helper to update drop indicators for a target block
function updateDropIndicators(targetBlock, mouseY) {
  const rect = targetBlock.getBoundingClientRect();
  const threshold = rect.top + rect.height / 2;
  const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
  const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
  if (mouseY < threshold) {
    dropIndicatorAbove.style.display = 'block';
    dropIndicatorAbove.classList.add('active');
    dropIndicatorBelow.style.display = 'none';
    dropIndicatorBelow.classList.remove('active');
    targetBlock.classList.add('drag-over-above');
    targetBlock.classList.remove('drag-over');
  } else {
    dropIndicatorAbove.style.display = 'none';
    dropIndicatorAbove.classList.remove('active');
    dropIndicatorBelow.style.display = 'block';
    dropIndicatorBelow.classList.add('active');
    targetBlock.classList.add('drag-over');
    targetBlock.classList.remove('drag-over-above');
  }
}

// Then, simplify the event handler:
function handleBlockDragEnter(e) {
  e.preventDefault();
  if (!isDraggingBlock || !draggedBlock) return;
  const targetBlock = e.target.closest('.block');
  if (!targetBlock || targetBlock === draggedBlock) return;
  updateDropIndicators(targetBlock, e.clientY);
}
```

Similarly, consider extracting form element creation into a small helper. For example:

```js
// Helper to create a form group from a configuration field
function createFormGroup(labelText, inputElement, required = false) {
  const formGroup = document.createElement('div');
  formGroup.className = 'mb-3';

  const label = document.createElement('label');
  label.className = 'form-label';
  label.textContent = labelText;
  if (required) {
    label.innerHTML += ' <span class="text-danger">*</span>';
  }

  formGroup.appendChild(label);
  formGroup.appendChild(inputElement);
  return formGroup;
}
```

Using these helpers can reduce duplication and deeply nested conditionals without reverting any functionality. These small modular changes can improve maintainability and readability while keeping all functionality intact.
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-object-destructuring))

```suggestion
const {blockTypes} = window;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (isDraggingBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!isDraggingBlock || !draggedBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!isDraggingBlock || !draggedBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!targetBlock || targetBlock === draggedBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!targetBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!isDraggingBlock || !draggedBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!targetBlock || targetBlock === draggedBlock) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!block) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
        if (key === 'block_name') {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
+        
+        const formGroup = document.createElement('div');
+        formGroup.className = 'mb-3';
+        
+        const label = document.createElement('label');
+        label.className = 'form-label';
+        label.textContent = key;
+        if (config.required) {
+            label.innerHTML += ' <span class="text-danger">*</span>';
+        }
+        
+        let input;
+        if (config.type === 'number') {
+            input = document.createElement('input');
+            input.type = 'number';
+            input.className = 'form-control';
+            input.value = block.config[key] || config.default || '';
+        } else if (config.type === 'array') {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = (block.config[key] || []).join(', ');
+            input.placeholder = 'Comma-separated values';
+        } else if (config.type === 'object') {
+            input = document.createElement('textarea');
+            input.className = 'form-control';
+            input.value = JSON.stringify(block.config[key] || {}, null, 2);
+            input.rows = 3;
+            input.placeholder = 'JSON object';
+        } else if (config.enum) {
+            input = document.createElement('select');
+            input.className = 'form-select';
+            config.enum.forEach(option => {
+                const optionElement = document.createElement('option');
+                optionElement.value = option;
+                optionElement.textContent = option;
+                input.appendChild(optionElement);
+            });
+            input.value = block.config[key] || '';
+        } else {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = block.config[key] || '';
+        }
+        
+        input.name = key;
+        input.required = config.required;
+        
+        formGroup.appendChild(label);
+        formGroup.appendChild(input);
+        form.appendChild(formGroup);
+    });
+
+    blockConfigModal.show();
+}
+
+function saveBlockConfig() {
+    const form = document.getElementById('blockConfigForm');
+    const formData = new FormData(form);
+    const config = {};
+    
+    for (const [key, value] of formData.entries()) {
+        if (key === 'block_name') {
+            config[key] = value;
+        } else {
+            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
+            const fieldConfig = blockInfo.config[key];
+            
+            if (fieldConfig.type === 'array') {
+                config[key] = value.split(',').map(v => v.trim()).filter(v => v);
+            } else if (fieldConfig.type === 'object') {
+                try {
+                    config[key] = JSON.parse(value);
+                } catch (e) {
+                    config[key] = {};
+                }
+            } else if (fieldConfig.type === 'number') {
+                config[key] = parseFloat(value);
+            } else {
+                config[key] = value;
+            }
+        }
+    }
+    
+    const block = blocks.find(b => b.id === currentBlockId);
+    if (block) {
+        block.config = config;
+    }
+    
+    blockConfigModal.hide();
+    renderBlocks();
+}
+
+function saveFlow() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+    
+    // Generate YAML
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            throw new Error(data.error);
+        }
+        
+        // Create and download YAML file
+        const blob = new Blob([data.yaml], { type: 'text/yaml' });
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = 'flow.yaml';
+        document.body.appendChild(a);
+        a.click();
+        window.URL.revokeObjectURL(url);
+        document.body.removeChild(a);
+    })
+    .catch(error => {
+        console.error('Error saving flow:', error);
+        alert('Error saving flow: ' + error.message);
+    });
+}
+
+function generateYAML() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        const yamlOutput = document.getElementById('yaml-output');
+        yamlOutput.textContent = data.yaml;
+        yamlOutput.style.display = 'block';
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error generating YAML');
+    });
+}
+
+function loadYAML() {
+    yamlInputModal.show();
+}
+
+function parseYAML() {
+    const yamlContent = document.getElementById('yamlInput').value;
+    
+    fetch('/api/parse_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ yaml: yamlContent })
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            alert(data.error);
+            return;
+        }
+        
+        blocks = data.blocks;
+        renderBlocks();
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error parsing YAML');
+    });
+}
+
+function moveBlock(blockId, direction) {
+    const currentIndex = blocks.findIndex(b => b.id === blockId);
+    if (currentIndex === -1) return;
path: web_interface/static/js/app.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (currentIndex === -1) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-04T14:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
path: web_interface/app.py
user: login: shivchander
body: You can get the available blocks using the registry: 

```python
from sdg_hub.blocks import BlockRegistry, Block
BlockRegistry.get_registry()
```
```python
{'Block': sdg_hub.blocks.block.Block,
 'FilterByValueBlock': sdg_hub.blocks.filterblock.FilterByValueBlock,
 'IterBlock': sdg_hub.blocks.iterblock.IterBlock,
 'LLMBlock': sdg_hub.blocks.llmblock.LLMBlock,
 'ConditionalLLMBlock': sdg_hub.blocks.llmblock.ConditionalLLMBlock,
 'LLMLogProbBlock': sdg_hub.blocks.llmblock.LLMLogProbBlock,
 'LLMMessagesBlock': sdg_hub.blocks.llmblock.LLMMessagesBlock,
 'SamplePopulatorBlock': sdg_hub.blocks.utilblocks.SamplePopulatorBlock,
 'SelectorBlock': sdg_hub.blocks.utilblocks.SelectorBlock,
 'CombineColumnsBlock': sdg_hub.blocks.utilblocks.CombineColumnsBlock,
 'FlattenColumnsBlock': sdg_hub.blocks.utilblocks.FlattenColumnsBlock,
 'DuplicateColumns': sdg_hub.blocks.utilblocks.DuplicateColumns,
 'RenameColumns': sdg_hub.blocks.utilblocks.RenameColumns,
 'SetToMajorityValue': sdg_hub.blocks.utilblocks.SetToMajorityValue}
 ```
created_at: 2025-05-23T20:12:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
+    'llm': {
+        'name': 'LLM Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'config_path': {'type': 'string', 'required': True},
+            'model_id': {'type': 'string', 'required': True},
+            'output_cols': {'type': 'array', 'required': True},
+            'gen_kwargs': {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'default': 0.7},
+                    'max_tokens': {'type': 'number', 'default': 2048}
+                }
+            },
+            'drop_duplicates': {'type': 'array', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'filter': {
+        'name': 'Filter Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'filter_column': {'type': 'string', 'required': True},
+            'filter_value': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True},
+            'convert_dtype': {'type': 'string', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False},
+            'drop_columns': {'type': 'array', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'iter': {
+        'name': 'Iteration Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'iterations': {'type': 'number', 'required': True},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'rm': {
+        'name': 'Retrieval Model Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'model': {'type': 'string', 'required': True},
+            'query_field': {'type': 'string', 'required': True},
+            'top_k': {'type': 'number', 'required': False, 'default': 5}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'util': {
path: web_interface/app.py
user: login: shivchander
body: This is not a valid block - rather this is the whole group of blocks
created_at: 2025-05-23T20:14:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
+    'llm': {
+        'name': 'LLM Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'config_path': {'type': 'string', 'required': True},
+            'model_id': {'type': 'string', 'required': True},
+            'output_cols': {'type': 'array', 'required': True},
+            'gen_kwargs': {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'default': 0.7},
+                    'max_tokens': {'type': 'number', 'default': 2048}
+                }
+            },
+            'drop_duplicates': {'type': 'array', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'filter': {
+        'name': 'Filter Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'filter_column': {'type': 'string', 'required': True},
+            'filter_value': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True},
+            'convert_dtype': {'type': 'string', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False},
+            'drop_columns': {'type': 'array', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'iter': {
+        'name': 'Iteration Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'iterations': {'type': 'number', 'required': True},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'rm': {
+        'name': 'Retrieval Model Block',
path: web_interface/app.py
user: login: shivchander
body: This is not a valid block 
created_at: 2025-05-23T20:14:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
path: web_interface/app.py
user: login: shivchander
body: Is there a way we could use the registry instead of manually adding the block types?
created_at: 2025-05-23T20:15:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,3 @@
+flask==3.0.0
path: web_interface/requirements.txt
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix Flask version inconsistency between requirements files.**

There's a version mismatch: main `requirements.txt` specifies `flask==3.0.2` while this file uses `flask==3.0.0`. This could cause dependency conflicts.


Apply this diff to align with the main requirements file:

```diff
-flask==3.0.0
+flask==3.0.2
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
-flask==3.0.0
+flask==3.0.2
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/requirements.txt at line 1, update the Flask version from 3.0.0
to 3.0.2 to match the main requirements.txt file and avoid dependency conflicts.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
+    'llm': {
+        'name': 'LLM Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'config_path': {'type': 'string', 'required': True},
+            'model_id': {'type': 'string', 'required': True},
+            'output_cols': {'type': 'array', 'required': True},
+            'gen_kwargs': {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'default': 0.7},
+                    'max_tokens': {'type': 'number', 'default': 2048}
+                }
+            },
+            'drop_duplicates': {'type': 'array', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'filter': {
+        'name': 'Filter Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'filter_column': {'type': 'string', 'required': True},
+            'filter_value': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True},
+            'convert_dtype': {'type': 'string', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False},
+            'drop_columns': {'type': 'array', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'iter': {
+        'name': 'Iteration Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'iterations': {'type': 'number', 'required': True},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'rm': {
+        'name': 'Retrieval Model Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'model': {'type': 'string', 'required': True},
+            'query_field': {'type': 'string', 'required': True},
+            'top_k': {'type': 'number', 'required': False, 'default': 5}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'util': {
+        'name': 'Utility Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True, 'enum': ['map', 'reduce', 'filter']},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    }
+}
+
+@app.route('/')
+def index():
+    """Render the main interface page with block types."""
+    try:
+        return render_template('index.html', block_types=BLOCK_TYPES)
+    except Exception as e:
+        print(f"Error rendering template: {str(e)}")
+        return str(e), 500
+
+@app.route('/api/blocks', methods=['GET'])
+def get_blocks():
+    return jsonify(BLOCK_TYPES)
+
+@app.route('/api/generate_yaml', methods=['POST'])
+def generate_yaml():
+    """
+    Generate YAML from the flow configuration.
+    
+    Converts the JSON flow data into the correct YAML format for SDG.
+    Each block is converted to the appropriate YAML structure with block_type and block_config.
+    """
+    try:
+        flow_data = request.json
+        if not flow_data:
+            return jsonify({'error': 'No data provided'}), 400
+        
+        # Convert the flow data to the correct YAML format
+        yaml_blocks = []
+        
+        # Process blocks in the order they appear in the flow
+        for block in flow_data['blocks']:
+            block_type = block['type']
+            block_config = block['config']
+            
+            # Create the block entry in the correct format
+            yaml_block = {
+                'block_type': f"{block_type.upper()}Block",
+                'block_config': {
+                    'block_name': block_config.get('block_name', f"{block_type}_{len(yaml_blocks)}"),
+                    **{k: v for k, v in block_config.items() if k != 'block_name'}
+                }
+            }
+            
+            # Add drop_duplicates if specified
+            if 'drop_duplicates' in block_config:
+                yaml_block['drop_duplicates'] = block_config['drop_duplicates']
+            
+            yaml_blocks.append(yaml_block)
+        
+        # Convert to YAML format
+        yaml_config = yaml.dump(yaml_blocks, default_flow_style=False)
+        return jsonify({'yaml': yaml_config})
+    except Exception as e:
+        print(f"Error generating YAML: {str(e)}")
+        return jsonify({'error': str(e)}), 500
+
+@app.route('/api/parse_yaml', methods=['POST'])
+def parse_yaml():
+    """
+    Parse YAML into flow configuration.
+    
+    Converts YAML flow data into the format expected by the web interface.
+    Validates block types and creates appropriate block configurations.
+    """
+    try:
+        yaml_content = request.json.get('yaml', '')
+        if not yaml_content:
+            return jsonify({'error': 'No YAML content provided'}), 400
+            
+        yaml_blocks = yaml.safe_load(yaml_content)
+        
+        if not isinstance(yaml_blocks, list):
+            return jsonify({'error': 'Invalid YAML format. Expected a list of blocks.'}), 400
+        
+        # Convert YAML blocks to canvas format
+        blocks = []
+        
+        # First pass: create blocks
+        for i, yaml_block in enumerate(yaml_blocks):
+            block_type = yaml_block.get('block_type', '').lower().replace('block', '')
+            if block_type not in BLOCK_TYPES:
+                return jsonify({'error': f'Unknown block type: {block_type}'}), 400
+            
+            block_config = yaml_block.get('block_config', {})
+            
+            # Create block with position
+            block = {
+                'id': i + 1,  # Use index as ID
+                'type': block_type,
+                'name': block_config.get('block_name', f"{block_type}_{i}"),
+                'config': block_config,
+                'position': {
+                    'x': i * 250,  # Position blocks horizontally
+                    'y': 100
+                }
+            }
+            blocks.append(block)
+        
+        return jsonify({
+            'blocks': blocks,
+            'connections': [],
+            'firstBlockId': blocks[0]['id'] if blocks else None
+        })
+    
+    except yaml.YAMLError as e:
+        print(f"YAML parsing error: {str(e)}")
+        return jsonify({'error': f'Invalid YAML: {str(e)}'}), 400
+    except Exception as e:
+        print(f"Error parsing YAML: {str(e)}")
+        return jsonify({'error': f'Error parsing YAML: {str(e)}'}), 500
+
+if __name__ == '__main__':
+    app.run(debug=True, port=8080) 
path: web_interface/app.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Disable debug mode for production deployment.**

Debug mode should never be enabled in production as it exposes sensitive information and poses security risks.


Apply this diff to use environment-based configuration:

```diff
if __name__ == '__main__':
-    app.run(debug=True, port=8080)
+    debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
+    app.run(debug=debug_mode, port=8080)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
if __name__ == '__main__':
    debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
    app.run(debug=debug_mode, port=8080)
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

208-208: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>
<details>
<summary>🪛 ast-grep (0.38.1)</summary>

[warning] 208-208: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True, port=8080)
Note: [CWE-489] Active Debug Code. [REFERENCES]
    - https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/app.py at lines 208 to 209, debug mode is enabled directly in
app.run, which is unsafe for production. Modify the code to disable debug mode
by default and enable it only when an environment variable (e.g., FLASK_ENV)
indicates a development environment. Use environment-based configuration to set
the debug parameter dynamically, ensuring debug mode is off in production
deployments.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+
+# Initialize Flask application
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)  # Generate a random secret key
+
+# Define available block types and their configuration schemas
+# Each block type has a name, configuration options, and connection rules
+BLOCK_TYPES = {
+    'llm': {
+        'name': 'LLM Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'config_path': {'type': 'string', 'required': True},
+            'model_id': {'type': 'string', 'required': True},
+            'output_cols': {'type': 'array', 'required': True},
+            'gen_kwargs': {
+                'type': 'object',
+                'required': False,
+                'properties': {
+                    'temperature': {'type': 'number', 'default': 0.7},
+                    'max_tokens': {'type': 'number', 'default': 2048}
+                }
+            },
+            'drop_duplicates': {'type': 'array', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'filter': {
+        'name': 'Filter Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'filter_column': {'type': 'string', 'required': True},
+            'filter_value': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True},
+            'convert_dtype': {'type': 'string', 'required': False},
+            'batch_kwargs': {'type': 'object', 'required': False},
+            'drop_columns': {'type': 'array', 'required': False}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'iter': {
+        'name': 'Iteration Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'iterations': {'type': 'number', 'required': True},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'rm': {
+        'name': 'Retrieval Model Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'model': {'type': 'string', 'required': True},
+            'query_field': {'type': 'string', 'required': True},
+            'top_k': {'type': 'number', 'required': False, 'default': 5}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    },
+    'util': {
+        'name': 'Utility Block',
+        'config': {
+            'block_name': {'type': 'string', 'required': True},
+            'operation': {'type': 'string', 'required': True, 'enum': ['map', 'reduce', 'filter']},
+            'input_field': {'type': 'string', 'required': True}
+        },
+        'connections': {
+            'input': ['llm', 'filter', 'iter', 'rm', 'util'],
+            'output': ['llm', 'filter', 'iter', 'util']
+        }
+    }
+}
+
+@app.route('/')
+def index():
+    """Render the main interface page with block types."""
+    try:
+        return render_template('index.html', block_types=BLOCK_TYPES)
+    except Exception as e:
+        print(f"Error rendering template: {str(e)}")
+        return str(e), 500
+
+@app.route('/api/blocks', methods=['GET'])
+def get_blocks():
+    return jsonify(BLOCK_TYPES)
+
+@app.route('/api/generate_yaml', methods=['POST'])
+def generate_yaml():
+    """
+    Generate YAML from the flow configuration.
+    
+    Converts the JSON flow data into the correct YAML format for SDG.
+    Each block is converted to the appropriate YAML structure with block_type and block_config.
+    """
+    try:
+        flow_data = request.json
+        if not flow_data:
+            return jsonify({'error': 'No data provided'}), 400
+        
+        # Convert the flow data to the correct YAML format
+        yaml_blocks = []
+        
+        # Process blocks in the order they appear in the flow
+        for block in flow_data['blocks']:
+            block_type = block['type']
+            block_config = block['config']
+            
+            # Create the block entry in the correct format
+            yaml_block = {
+                'block_type': f"{block_type.upper()}Block",
+                'block_config': {
+                    'block_name': block_config.get('block_name', f"{block_type}_{len(yaml_blocks)}"),
+                    **{k: v for k, v in block_config.items() if k != 'block_name'}
+                }
+            }
path: web_interface/app.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Validate block type format in YAML generation.**

The code assumes all block types should have "Block" suffix, but this should be validated against the actual registry to ensure compatibility.

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the actual block class naming pattern in the registry
ast-grep --pattern 'class $NAME(Block):' | head -10
rg "class.*Block.*:" -A 2 | head -20
```

Length of output: 1375

---

🏁 Script executed:

```shell
#!/bin/bash
# List all classes inheriting from Block in src/sdg_hub/blocks
rg "class .*Block\(" -H src/sdg_hub/blocks

# Show context around yaml_block definition in web_interface/app.py
rg -n -C3 "yaml_block" web_interface/app.py
```

Length of output: 3087

---

```bash
#!/bin/bash
# Locate definition of BLOCK_TYPES in the repository
rg -n "BLOCK_TYPES" -H .

# Show surrounding lines for context where BLOCK_TYPES is defined
rg -n -C5 "BLOCK_TYPES" -H .
```

---

```bash
#!/bin/bash
# Find where BLOCK_TYPES is defined or imported
rg -n "BLOCK_TYPES" -H .

# Show surrounding context for BLOCK_TYPES
rg -n -C5 "BLOCK_TYPES" -H .

# Locate the assignment of block_type in the YAML generation loop
rg -n -C3 "block_type\s*=" web_interface/app.py
```

---

```bash
#!/bin/bash
# Show lines around where block_type is assigned in YAML generation
sed -n '115,130p' web_interface/app.py
```

---

```bash
#!/bin/bash
# Locate the BLOCK_TYPES constant in the repo
rg -n "BLOCK_TYPES" -H .

# Show its definition with context
rg -n -C5 "BLOCK_TYPES" -H .

# Verify where and how block_type is set before YAML generation
rg -n -C5 "for block in flow_data" -H web_interface/app.py
rg -n -C5 "block_type" -H web_interface/app.py
```

---



</details>

**Add block type validation in YAML generation**

Before appending the `"Block"` suffix, ensure the incoming `block_type` matches one of the registered types to prevent emitting invalid YAML.

• File: web_interface/app.py  
  – Just before line 132 (where `yaml_block` is constructed), insert a check such as:  
  ```python
  # Validate that the block type is known
  if block_type.lower() not in BLOCK_TYPES:
      return jsonify({'error': f'Unknown block type: {block_type}'}), 400
  ```
• Then proceed with:
  ```diff
  - 'block_type': f"{block_type.upper()}Block",
  + 'block_type': f"{block_type.upper()}Block",
  ```
  (now guarded by the preceding validation)

This ensures any `block_type` you format to `"XBlock"` actually exists in your registry.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

133-133: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

134-134: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

135-135: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

135-135: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

136-136: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/app.py around lines 132 to 138, the code appends "Block" to
block_type without validating if block_type is registered, risking invalid YAML
output. Fix this by adding a validation step before constructing yaml_block:
check if block_type.lower() is in the BLOCK_TYPES registry, and if not, return a
JSON error response with status 400. This ensures only known block types are
processed and prevents generating invalid YAML.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
+        
+        const formGroup = document.createElement('div');
+        formGroup.className = 'mb-3';
+        
+        const label = document.createElement('label');
+        label.className = 'form-label';
+        label.textContent = key;
+        if (config.required) {
+            label.innerHTML += ' <span class="text-danger">*</span>';
+        }
+        
+        let input;
+        if (config.type === 'number') {
+            input = document.createElement('input');
+            input.type = 'number';
+            input.className = 'form-control';
+            input.value = block.config[key] || config.default || '';
+        } else if (config.type === 'array') {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = (block.config[key] || []).join(', ');
+            input.placeholder = 'Comma-separated values';
+        } else if (config.type === 'object') {
+            input = document.createElement('textarea');
+            input.className = 'form-control';
+            input.value = JSON.stringify(block.config[key] || {}, null, 2);
+            input.rows = 3;
+            input.placeholder = 'JSON object';
+        } else if (config.enum) {
+            input = document.createElement('select');
+            input.className = 'form-select';
+            config.enum.forEach(option => {
+                const optionElement = document.createElement('option');
+                optionElement.value = option;
+                optionElement.textContent = option;
+                input.appendChild(optionElement);
+            });
+            input.value = block.config[key] || '';
+        } else {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = block.config[key] || '';
+        }
+        
+        input.name = key;
+        input.required = config.required;
+        
+        formGroup.appendChild(label);
+        formGroup.appendChild(input);
+        form.appendChild(formGroup);
+    });
+
+    blockConfigModal.show();
+}
+
+function saveBlockConfig() {
+    const form = document.getElementById('blockConfigForm');
+    const formData = new FormData(form);
+    const config = {};
+    
+    for (const [key, value] of formData.entries()) {
+        if (key === 'block_name') {
+            config[key] = value;
+        } else {
+            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
+            const fieldConfig = blockInfo.config[key];
+            
+            if (fieldConfig.type === 'array') {
+                config[key] = value.split(',').map(v => v.trim()).filter(v => v);
+            } else if (fieldConfig.type === 'object') {
+                try {
+                    config[key] = JSON.parse(value);
+                } catch (e) {
+                    config[key] = {};
+                }
+            } else if (fieldConfig.type === 'number') {
+                config[key] = parseFloat(value);
+            } else {
+                config[key] = value;
+            }
+        }
+    }
+    
+    const block = blocks.find(b => b.id === currentBlockId);
+    if (block) {
+        block.config = config;
+    }
+    
+    blockConfigModal.hide();
+    renderBlocks();
+}
+
+function saveFlow() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+    
+    // Generate YAML
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            throw new Error(data.error);
+        }
+        
+        // Create and download YAML file
+        const blob = new Blob([data.yaml], { type: 'text/yaml' });
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = 'flow.yaml';
+        document.body.appendChild(a);
+        a.click();
+        window.URL.revokeObjectURL(url);
+        document.body.removeChild(a);
+    })
+    .catch(error => {
+        console.error('Error saving flow:', error);
+        alert('Error saving flow: ' + error.message);
+    });
+}
+
+function generateYAML() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        const yamlOutput = document.getElementById('yaml-output');
+        yamlOutput.textContent = data.yaml;
+        yamlOutput.style.display = 'block';
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error generating YAML');
+    });
+}
+
+function loadYAML() {
+    yamlInputModal.show();
+}
+
+function parseYAML() {
+    const yamlContent = document.getElementById('yamlInput').value;
+    
+    fetch('/api/parse_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ yaml: yamlContent })
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            alert(data.error);
+            return;
+        }
+        
+        blocks = data.blocks;
+        renderBlocks();
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error parsing YAML');
+    });
+}
path: web_interface/static/js/app.js
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add input validation and improve error handling for YAML parsing.**

The current implementation doesn't validate the YAML input before sending it to the server, and error handling could be more specific.


```diff
 function parseYAML() {
     const yamlContent = document.getElementById('yamlInput').value;
+    
+    // Validate input
+    if (!yamlContent.trim()) {
+        alert('Please enter YAML content');
+        return;
+    }
     
     fetch('/api/parse_yaml', {
         method: 'POST',
@@ -407,16 +411,21 @@
     .then(response => response.json())
     .then(data => {
         if (data.error) {
-            alert(data.error);
+            alert('YAML Parse Error: ' + data.error);
             return;
         }
         
         blocks = data.blocks;
         renderBlocks();
+        yamlInputModal.hide();
+        document.getElementById('yamlInput').value = '';
+        alert('YAML loaded successfully!');
     })
     .catch(error => {
         console.error('Error:', error);
-        alert('Error parsing YAML');
+        alert('Error parsing YAML: ' + error.message);
     });
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
function parseYAML() {
    const yamlContent = document.getElementById('yamlInput').value;
    
    // Validate input
    if (!yamlContent.trim()) {
        alert('Please enter YAML content');
        return;
    }
    
    fetch('/api/parse_yaml', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ yaml: yamlContent })
    })
    .then(response => response.json())
    .then(data => {
        if (data.error) {
            alert('YAML Parse Error: ' + data.error);
            return;
        }
        
        blocks = data.blocks;
        renderBlocks();
        yamlInputModal.hide();
        document.getElementById('yamlInput').value = '';
        alert('YAML loaded successfully!');
    })
    .catch(error => {
        console.error('Error:', error);
        alert('Error parsing YAML: ' + error.message);
    });
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/static/js/app.js between lines 396 and 420, add validation to
check if the YAML input is empty or invalid before making the fetch request to
avoid unnecessary server calls. Enhance error handling by distinguishing between
network errors and server response errors, providing more specific alert
messages to the user. Implement these checks before sending the request and
improve the catch block to handle different error scenarios clearly.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
path: web_interface/static/js/app.js
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Implement the missing getOrderedBlocks function.**

The code references `getOrderedBlocks()` function but it's not defined anywhere in the file. This will cause a runtime error.


Add the missing function:

```diff
+function getOrderedBlocks() {
+    return [...blocks];
+}
+
 function handleBlockDrop(e) {
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/static/js/app.js around lines 164 to 165, the function
getOrderedBlocks is called but not defined, causing a runtime error. Implement
the getOrderedBlocks function to return the blocks in the desired order,
ensuring it matches the expected data structure and logic used elsewhere in the
code. Define this function above its usage or in an appropriate module to avoid
reference errors.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,518 @@
+// Initialize block types from the server
+const blockTypes = window.blockTypes;
+let blocks = [];
+let currentBlockId = null;
+let isDraggingBlock = false;
+let draggedBlock = null;
+let dragOffset = { x: 0, y: 0 };
+const blockConfigModal = new bootstrap.Modal(document.getElementById('blockConfigModal'));
+const yamlInputModal = new bootstrap.Modal(document.getElementById('yamlInputModal'));
+
+// Make blocks draggable
+document.querySelectorAll('.block[data-block-type]').forEach(block => {
+    block.setAttribute('draggable', 'true');
+    block.addEventListener('dragstart', handleDragStart);
+});
+
+const flowCanvas = document.getElementById('flow-canvas');
+flowCanvas.addEventListener('dragover', handleDragOver);
+flowCanvas.addEventListener('dragenter', handleDragEnter);
+flowCanvas.addEventListener('dragleave', handleDragLeave);
+flowCanvas.addEventListener('drop', handleDrop);
+
+function handleDragStart(e) {
+    e.dataTransfer.setData('text/plain', e.target.dataset.blockType);
+    e.dataTransfer.effectAllowed = 'copy';
+    e.target.classList.add('dragging');
+}
+
+function handleDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'copy';
+}
+
+function handleDragEnter(e) {
+    e.preventDefault();
+    flowCanvas.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    if (!flowCanvas.contains(e.relatedTarget)) {
+        flowCanvas.classList.remove('drag-over');
+    }
+}
+
+function handleDrop(e) {
+    e.preventDefault();
+    flowCanvas.classList.remove('drag-over');
+    document.querySelector('.dragging')?.classList.remove('dragging');
+    
+    const blockType = e.dataTransfer.getData('text/plain');
+    const blockInfo = blockTypes[blockType];
+    
+    // Calculate position relative to the canvas
+    const rect = flowCanvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+    
+    const block = {
+        id: Date.now(),
+        type: blockType,
+        name: blockInfo.name,
+        config: {},
+        position: { x, y }
+    };
+    
+    blocks.push(block);
+    renderBlocks();
+}
+
+function handleBlockDragStart(e) {
+    if (isDraggingBlock) return;
+    
+    isDraggingBlock = true;
+    draggedBlock = e.target;
+    draggedBlock.classList.add('dragging');
+    
+    // Set the drag image to be transparent
+    const dragImage = document.createElement('div');
+    dragImage.style.opacity = '0';
+    document.body.appendChild(dragImage);
+    e.dataTransfer.setDragImage(dragImage, 0, 0);
+    setTimeout(() => document.body.removeChild(dragImage), 0);
+}
+
+function handleBlockDragEnd(e) {
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    isDraggingBlock = false;
+    draggedBlock.classList.remove('dragging');
+    
+    // Hide all drop indicators and remove drag-over classes
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    draggedBlock = null;
+}
+
+function handleBlockDragOver(e) {
+    e.preventDefault();
+    e.dataTransfer.dropEffect = 'move';
+}
+
+function handleBlockDragEnter(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    
+    // Show appropriate drop indicator and add visual feedback
+    const dropIndicatorAbove = targetBlock.querySelector('.drop-indicator.above');
+    const dropIndicatorBelow = targetBlock.querySelector('.drop-indicator.below');
+    
+    if (mouseY < threshold) {
+        dropIndicatorAbove.style.display = 'block';
+        dropIndicatorAbove.classList.add('active');
+        dropIndicatorBelow.style.display = 'none';
+        dropIndicatorBelow.classList.remove('active');
+        targetBlock.classList.add('drag-over-above');
+        targetBlock.classList.remove('drag-over');
+    } else {
+        dropIndicatorAbove.style.display = 'none';
+        dropIndicatorAbove.classList.remove('active');
+        dropIndicatorBelow.style.display = 'block';
+        dropIndicatorBelow.classList.add('active');
+        targetBlock.classList.add('drag-over');
+        targetBlock.classList.remove('drag-over-above');
+    }
+}
+
+function handleBlockDragLeave(e) {
+    e.preventDefault();
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock) return;
+    
+    // Hide drop indicators and remove visual feedback
+    targetBlock.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    targetBlock.classList.remove('drag-over', 'drag-over-above');
+}
+
+function handleBlockDrop(e) {
+    e.preventDefault();
+    if (!isDraggingBlock || !draggedBlock) return;
+    
+    const targetBlock = e.target.closest('.block');
+    if (!targetBlock || targetBlock === draggedBlock) return;
+    
+    const draggedIndex = parseInt(draggedBlock.dataset.index);
+    const targetIndex = parseInt(targetBlock.dataset.index);
+    
+    // Get the ordered blocks
+    const orderedBlocks = getOrderedBlocks();
+    
+    // Remove the dragged block
+    const [draggedBlockData] = orderedBlocks.splice(draggedIndex, 1);
+    
+    // Determine the new position
+    const rect = targetBlock.getBoundingClientRect();
+    const mouseY = e.clientY;
+    const threshold = rect.top + rect.height / 2;
+    const newIndex = mouseY < threshold ? targetIndex : targetIndex + 1;
+    
+    // Insert the block at the new position
+    orderedBlocks.splice(newIndex, 0, draggedBlockData);
+    
+    // Update the blocks array
+    blocks = orderedBlocks;
+    
+    // Hide all drop indicators and remove visual feedback
+    document.querySelectorAll('.drop-indicator').forEach(indicator => {
+        indicator.style.display = 'none';
+        indicator.classList.remove('active');
+    });
+    document.querySelectorAll('.block').forEach(block => {
+        block.classList.remove('drag-over', 'drag-over-above');
+    });
+    
+    // Re-render the blocks with a smooth transition
+    renderBlocks();
+}
+
+function removeBlock(id) {
+    blocks = blocks.filter(block => block.id !== id);
+    renderBlocks();
+}
+
+function configureBlock(id) {
+    currentBlockId = id;
+    const block = blocks.find(b => b.id === id);
+    if (!block) return;
+
+    const blockInfo = blockTypes[block.type];
+    const form = document.getElementById('blockConfigForm');
+    form.innerHTML = '';
+
+    // Add block name field
+    const nameGroup = document.createElement('div');
+    nameGroup.className = 'mb-3';
+    
+    const nameLabel = document.createElement('label');
+    nameLabel.className = 'form-label';
+    nameLabel.textContent = 'Block Name';
+    nameLabel.innerHTML += ' <span class="text-danger">*</span>';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.className = 'form-control';
+    nameInput.name = 'block_name';
+    nameInput.value = block.config.block_name || `${block.type}_${blocks.indexOf(block)}`;
+    nameInput.required = true;
+    
+    nameGroup.appendChild(nameLabel);
+    nameGroup.appendChild(nameInput);
+    form.appendChild(nameGroup);
+
+    // Add other configuration fields
+    Object.entries(blockInfo.config).forEach(([key, config]) => {
+        if (key === 'block_name') return; // Skip block_name as we already added it
+        
+        const formGroup = document.createElement('div');
+        formGroup.className = 'mb-3';
+        
+        const label = document.createElement('label');
+        label.className = 'form-label';
+        label.textContent = key;
+        if (config.required) {
+            label.innerHTML += ' <span class="text-danger">*</span>';
+        }
+        
+        let input;
+        if (config.type === 'number') {
+            input = document.createElement('input');
+            input.type = 'number';
+            input.className = 'form-control';
+            input.value = block.config[key] || config.default || '';
+        } else if (config.type === 'array') {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = (block.config[key] || []).join(', ');
+            input.placeholder = 'Comma-separated values';
+        } else if (config.type === 'object') {
+            input = document.createElement('textarea');
+            input.className = 'form-control';
+            input.value = JSON.stringify(block.config[key] || {}, null, 2);
+            input.rows = 3;
+            input.placeholder = 'JSON object';
+        } else if (config.enum) {
+            input = document.createElement('select');
+            input.className = 'form-select';
+            config.enum.forEach(option => {
+                const optionElement = document.createElement('option');
+                optionElement.value = option;
+                optionElement.textContent = option;
+                input.appendChild(optionElement);
+            });
+            input.value = block.config[key] || '';
+        } else {
+            input = document.createElement('input');
+            input.type = 'text';
+            input.className = 'form-control';
+            input.value = block.config[key] || '';
+        }
+        
+        input.name = key;
+        input.required = config.required;
+        
+        formGroup.appendChild(label);
+        formGroup.appendChild(input);
+        form.appendChild(formGroup);
+    });
+
+    blockConfigModal.show();
+}
+
+function saveBlockConfig() {
+    const form = document.getElementById('blockConfigForm');
+    const formData = new FormData(form);
+    const config = {};
+    
+    for (const [key, value] of formData.entries()) {
+        if (key === 'block_name') {
+            config[key] = value;
+        } else {
+            const blockInfo = blockTypes[blocks.find(b => b.id === currentBlockId).type];
+            const fieldConfig = blockInfo.config[key];
+            
+            if (fieldConfig.type === 'array') {
+                config[key] = value.split(',').map(v => v.trim()).filter(v => v);
+            } else if (fieldConfig.type === 'object') {
+                try {
+                    config[key] = JSON.parse(value);
+                } catch (e) {
+                    config[key] = {};
+                }
+            } else if (fieldConfig.type === 'number') {
+                config[key] = parseFloat(value);
+            } else {
+                config[key] = value;
+            }
+        }
+    }
+    
+    const block = blocks.find(b => b.id === currentBlockId);
+    if (block) {
+        block.config = config;
+    }
+    
+    blockConfigModal.hide();
+    renderBlocks();
+}
+
+function saveFlow() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+    
+    // Generate YAML
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            throw new Error(data.error);
+        }
+        
+        // Create and download YAML file
+        const blob = new Blob([data.yaml], { type: 'text/yaml' });
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = 'flow.yaml';
+        document.body.appendChild(a);
+        a.click();
+        window.URL.revokeObjectURL(url);
+        document.body.removeChild(a);
+    })
+    .catch(error => {
+        console.error('Error saving flow:', error);
+        alert('Error saving flow: ' + error.message);
+    });
+}
+
+function generateYAML() {
+    const flowData = {
+        blocks: blocks.map(block => ({
+            type: block.type,
+            config: block.config
+        }))
+    };
+
+    fetch('/api/generate_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(flowData)
+    })
+    .then(response => response.json())
+    .then(data => {
+        const yamlOutput = document.getElementById('yaml-output');
+        yamlOutput.textContent = data.yaml;
+        yamlOutput.style.display = 'block';
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error generating YAML');
+    });
+}
+
+function loadYAML() {
+    yamlInputModal.show();
+}
+
+function parseYAML() {
+    const yamlContent = document.getElementById('yamlInput').value;
+    
+    fetch('/api/parse_yaml', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({ yaml: yamlContent })
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.error) {
+            alert(data.error);
+            return;
+        }
+        
+        blocks = data.blocks;
+        renderBlocks();
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        alert('Error parsing YAML');
+    });
+}
+
+function moveBlock(blockId, direction) {
+    const currentIndex = blocks.findIndex(b => b.id === blockId);
+    if (currentIndex === -1) return;
+    
+    let newIndex;
+    if (direction === 'up') {
+        newIndex = currentIndex - 1;
+    } else if (direction === 'down') {
+        newIndex = currentIndex + 1;
+    }
+    
+    // Check if the new index is valid
+    if (newIndex < 0 || newIndex >= blocks.length) {
+        return;
+    }
+    
+    // Swap blocks
+    [blocks[currentIndex], blocks[newIndex]] = [blocks[newIndex], blocks[currentIndex]];
+    renderBlocks();
+}
+
+function renderBlocks() {
+    flowCanvas.innerHTML = '';
+    
+    // Render blocks in their current order
+    blocks.forEach((block, index) => {
+        const blockElement = document.createElement('div');
+        blockElement.className = 'block';
+        blockElement.dataset.blockId = block.id;
+        blockElement.dataset.index = index;
+        
+        // Add drag handle
+        const dragHandle = document.createElement('div');
+        dragHandle.className = 'block-handle';
+        dragHandle.innerHTML = '<i class="fas fa-grip-vertical"></i>';
+        blockElement.appendChild(dragHandle);
+        
+        const blockContent = document.createElement('div');
+        blockContent.className = 'block-content';
+        
+        // Create name span
+        const nameSpan = document.createElement('span');
+        nameSpan.className = 'block-name';
+        
+        // Add block type
+        const typeSpan = document.createElement('span');
+        typeSpan.className = 'block-type';
+        typeSpan.textContent = block.type;
+        nameSpan.appendChild(typeSpan);
+        
+        // Add block name if it exists
+        if (block.config && block.config.block_name) {
+            const nameValueSpan = document.createElement('span');
+            nameValueSpan.className = 'block-name-value';
+            nameValueSpan.textContent = block.config.block_name;
+            nameSpan.appendChild(nameValueSpan);
+        }
+        
+        blockContent.appendChild(nameSpan);
+        
+        // Create buttons container
+        const buttonsContainer = document.createElement('div');
+        buttonsContainer.className = 'block-actions';
+        
+        // Create up button
+        const upButton = document.createElement('button');
+        upButton.className = 'btn btn-sm btn-outline-secondary';
+        upButton.innerHTML = '<i class="fas fa-arrow-up"></i>';
+        upButton.onclick = function() { moveBlock(block.id, 'up'); };
+        buttonsContainer.appendChild(upButton);
+        
+        // Create down button
+        const downButton = document.createElement('button');
+        downButton.className = 'btn btn-sm btn-outline-secondary';
+        downButton.innerHTML = '<i class="fas fa-arrow-down"></i>';
+        downButton.onclick = function() { moveBlock(block.id, 'down'); };
+        buttonsContainer.appendChild(downButton);
+        
+        // Create configure button
+        const configureButton = document.createElement('button');
+        configureButton.className = 'btn btn-sm btn-outline-primary';
+        configureButton.onclick = function() { configureBlock(block.id); };
+        configureButton.innerHTML = '<i class="fas fa-cog"></i>';
+        buttonsContainer.appendChild(configureButton);
+        
+        // Create remove button
+        const removeButton = document.createElement('button');
+        removeButton.className = 'btn btn-sm btn-outline-danger';
+        removeButton.onclick = function() { removeBlock(block.id); };
+        removeButton.innerHTML = '<i class="fas fa-times"></i>';
+        buttonsContainer.appendChild(removeButton);
+        
+        blockContent.appendChild(buttonsContainer);
+        blockElement.appendChild(blockContent);
+        flowCanvas.appendChild(blockElement);
+    });
+} 
path: web_interface/static/js/app.js
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add drag event handlers to dynamically created blocks.**

The dynamically created blocks in `renderBlocks()` need drag event handlers to support reordering functionality. Currently, only the palette blocks have drag handlers attached.


Add drag event handlers to the created blocks:

```diff
         blockElement.className = 'block';
         blockElement.dataset.blockId = block.id;
         blockElement.dataset.index = index;
+        
+        // Make block draggable for reordering
+        blockElement.draggable = true;
+        blockElement.addEventListener('dragstart', handleBlockDragStart);
+        blockElement.addEventListener('dragend', handleBlockDragEnd);
+        blockElement.addEventListener('dragover', handleBlockDragOver);
+        blockElement.addEventListener('dragenter', handleBlockDragEnter);
+        blockElement.addEventListener('dragleave', handleBlockDragLeave);
+        blockElement.addEventListener('drop', handleBlockDrop);
+        
+        // Add drop indicators
+        const dropIndicatorAbove = document.createElement('div');
+        dropIndicatorAbove.className = 'drop-indicator above';
+        blockElement.appendChild(dropIndicatorAbove);
+        
+        const dropIndicatorBelow = document.createElement('div');
+        dropIndicatorBelow.className = 'drop-indicator below';
+        blockElement.appendChild(dropIndicatorBelow);
         
         // Add drag handle
         const dragHandle = document.createElement('div');
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 473-473: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/static/js/app.js between lines 443 and 518, the dynamically
created blocks in renderBlocks() lack drag event handlers needed for reordering.
To fix this, add dragstart, dragover, and drop event listeners to each
blockElement or its drag handle within the loop. Implement handlers that
initiate dragging, allow dropping, and handle the reordering logic accordingly
to enable drag-and-drop functionality for these blocks.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T20:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+# SDG Flow Builder Web Interface
+
+A web-based interface for building and managing SDG flows using a drag-and-drop interface.
+
+## Features
+
+- Drag-and-drop block creation
+- Visual block reordering with up/down buttons
+- Block configuration through a modal interface
+- YAML generation and parsing
+- Support for multiple block types (LLM, Filter, Iteration, etc.)
+
+## Block Types
+
+### LLM Block
+- Used for language model operations
+- Configurable model ID, output columns, and generation parameters
+- Supports batch processing and duplicate handling
+
+### Filter Block
+- Filters data based on column values
+- Supports various operations (equals, greater than, contains)
+- Can convert data types and drop columns
+
+### Iteration Block
+- Handles iterative operations
+- Configurable number of iterations
+- Specifies input field for iteration
+
+### Retrieval Model Block
+- Manages retrieval operations
+- Configurable model and query field
+- Supports top-k retrieval
+
+### Utility Block
+- General-purpose operations
+- Supports map, reduce, and filter operations
+- Configurable input field
+
+## Usage
+
+1. Start the Flask server:
+```bash
+cd web_interface
+python app.py
+```
path: web_interface/README.md
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add dependency installation step.**
Before instructing users to start the Flask server, include a step such as:
```bash
pip install -r requirements.txt
```
to ensure all Python dependencies are installed prior to running `app.py`.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/README.md around lines 40 to 46, add a step before starting the
Flask server to install dependencies by running 'pip install -r
requirements.txt'. This ensures users have all required Python packages
installed before executing 'python app.py'.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T21:59:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,227 @@
+"""
+SDG Flow Builder Web Interface
+-----------------------------
+A Flask-based web interface for building and managing SDG flows.
+Provides a drag-and-drop interface for creating and configuring flow blocks.
+"""
+
+from flask import Flask, render_template, jsonify, request
+import yaml
+import os
+from sdg_hub.registry import BlockRegistry
+import inspect
+from typing import get_type_hints, get_origin, get_args, Any
+from sdg_hub.blocks import *  # Ensure all blocks are registered
path: web_interface/app.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Replace wildcard import with explicit imports.**

The wildcard import `from sdg_hub.blocks import *` can lead to namespace pollution and makes it difficult to track dependencies. This pattern can also cause issues with code analysis tools and IDE support.



Consider replacing with explicit imports or importing only what's needed for block registration:

```diff
-from sdg_hub.blocks import *  # Ensure all blocks are registered
+# Import specific block modules to ensure registration
+import sdg_hub.blocks.filterblock
+import sdg_hub.blocks.iterblock  
+import sdg_hub.blocks.llmblock
+import sdg_hub.blocks.utilblocks
```

Alternatively, if the wildcard import is necessary for block registration, add a comment explaining why and consider moving this responsibility to the registry initialization.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
-from sdg_hub.blocks import *  # Ensure all blocks are registered
+# Import specific block modules to ensure registration
+import sdg_hub.blocks.filterblock
+import sdg_hub.blocks.iterblock
+import sdg_hub.blocks.llmblock
+import sdg_hub.blocks.utilblocks
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In web_interface/app.py at line 14, replace the wildcard import 'from
sdg_hub.blocks import *' with explicit imports of only the necessary blocks used
for registration. Identify which specific blocks are required and import them
individually to avoid namespace pollution and improve code clarity. If the
wildcard import is essential for block registration, add a clear comment
explaining this necessity and consider relocating this import to the registry
initialization module to better encapsulate the registration logic.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T01:03:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-04T14:42:26Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request introduces a new Flask-based web interface (`web_interface/`) allowing users to visually build SDG flows. It uses vanilla JavaScript for frontend interactivity, enabling drag-and-drop block creation, reordering, and configuration via modals. Backend API endpoints handle the generation of YAML flow definitions from the visual representation and parsing uploaded YAML back into the interface.

#### Sequence Diagram for Generating/Saving YAML Flow

```mermaid
sequenceDiagram
    actor User
    participant BrowserJS as Browser (app.js)
    participant FlaskAPI as Flask Backend (/api/generate_yaml)

    User->>BrowserJS: Clicks "Generate YAML" or "Save Flow"
    BrowserJS->>BrowserJS: Collect block data from canvas into JSON
    BrowserJS->>FlaskAPI: POST /api/generate_yaml (Sends flow JSON)
    FlaskAPI->>FlaskAPI: Convert JSON to SDG YAML format
    FlaskAPI-->>BrowserJS: Respond with generated YAML string
    alt Generate YAML Button Clicked
        BrowserJS->>User: Display YAML in UI <pre> tag
    else Save Flow Button Clicked
        BrowserJS->>User: Trigger download of YAML file
    end
```

#### Sequence Diagram for Loading YAML Flow

```mermaid
sequenceDiagram
    actor User
    participant BrowserJS as Browser (app.js)
    participant FlaskAPI as Flask Backend (/api/parse_yaml)

    User->>BrowserJS: Clicks "Load YAML"
    BrowserJS->>User: Show YAML Input Modal
    User->>BrowserJS: Paste YAML content and click "Load"
    BrowserJS->>FlaskAPI: POST /api/parse_yaml (Sends YAML string)
    FlaskAPI->>FlaskAPI: Parse YAML string into block structure
    FlaskAPI-->>BrowserJS: Respond with parsed flow JSON
    BrowserJS->>BrowserJS: Update internal 'blocks' array
    BrowserJS->>User: Re-render blocks visually on canvas
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Added a Flask web application to serve the flow builder GUI. | <ul><li>Created a Flask app (`app.py`) with routes for serving the main page and API endpoints.</li><li>Defined block types and their schemas within the Flask app.</li><li>Added an HTML template (`index.html`) using Bootstrap for the layout.</li><li>Included CSS (`style.css`) for custom styling of blocks and the canvas.</li><li>Added necessary dependencies (`flask`, `pyyaml`) to requirements files.</li><li>Configured Flask app with basic settings and secret key management.</li></ul> | `web_interface/app.py`<br/>`web_interface/templates/index.html`<br/>`web_interface/static/css/style.css`<br/>`web_interface/requirements.txt`<br/>`requirements.txt` |
| Implemented frontend logic for visual flow building. | <ul><li>Added JavaScript (`app.js`) to handle drag-and-drop for adding blocks from a palette to a canvas.</li><li>Implemented drag-and-drop reordering of blocks within the canvas.</li><li>Added up/down buttons for alternative block reordering.</li><li>Created functionality to open a modal for configuring individual block parameters based on their type.</li><li>Implemented block removal functionality.</li><li>Added functions to render and update the visual representation of blocks on the canvas.</li></ul> | `web_interface/static/js/app.js`<br/>`web_interface/templates/index.html` |
| Added backend API endpoints for YAML generation and parsing. | <ul><li>Created `/api/generate_yaml` endpoint to convert the frontend's JSON block data into SDG YAML format.</li><li>Created `/api/parse_yaml` endpoint to convert uploaded YAML content into the JSON format used by the frontend.</li><li>Added JavaScript fetch calls to interact with these API endpoints for saving and loading flows.</li></ul> | `web_interface/app.py`<br/>`web_interface/static/js/app.js` |
| Added documentation for the new web interface. | <ul><li>Created a `README.md` within the `web_interface` directory explaining features, usage, and development details of the GUI.</li></ul> | `web_interface/README.md` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-23T20:41:42Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

A web-based SDG Flow Builder interface has been introduced, featuring a drag-and-drop UI for constructing configurable flows using various block types. The update adds a Flask backend, static assets (CSS, JS), a detailed README, requirements files, and a main HTML template. APIs enable YAML export/import and block configuration. A test script verifies block type configuration consistency.

## Changes

| File(s)                                             | Change Summary                                                                                                    |
|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| requirements.txt, web_interface/requirements.txt    | Added requirements specifying Flask, PyYAML, and Flask-WTF dependencies with pinned versions.                     |
| web_interface/README.md                             | Added comprehensive documentation for the SDG Flow Builder web interface, detailing features, usage, and structure.|
| web_interface/app.py                                | Added Flask app with routes for serving UI, providing block type schemas, generating YAML, and parsing YAML flows.|
| web_interface/static/css/style.css                  | Added CSS for drag-and-drop block UI including containers, blocks, drag states, drop indicators, and modals.     |
| web_interface/static/js/app.js                      | Added JavaScript implementing drag-and-drop, block creation/configuration, flow management, and API communication.|
| web_interface/templates/index.html                  | Added main HTML template integrating Bootstrap, FontAwesome, modals, and dynamic block lists for the UI.         |
| web_interface/test_block_types.py                   | Added test script to verify block configuration schemas against block class constructors for consistency.        |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Browser
    participant FlaskApp
    participant YAML_API

    User->>Browser: Loads SDG Flow Builder UI
    Browser->>FlaskApp: GET /
    FlaskApp-->>Browser: Render index.html with block_types JSON

    User->>Browser: Drags and configures blocks on canvas
    Browser->>Browser: Update block list and show config modal

    User->>Browser: Clicks "Generate YAML"
    Browser->>YAML_API: POST /api/generate_yaml (flow JSON)
    YAML_API-->>Browser: Return YAML string

    User->>Browser: Clicks "Load YAML"
    Browser->>YAML_API: POST /api/parse_yaml (YAML content)
    YAML_API-->>Browser: Return flow JSON
    Browser->>Browser: Update UI with parsed blocks
```

## Poem

> In a meadow of code, I hop and I play,  
> Building block flows in a drag-and-drop way.  
> With Flask as my burrow, and YAML my song,  
> I leap through the canvas, where blocks all belong.  
> 🐇✨  
> Hooray for new features—let’s flow right along!
"""

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNxU3bABsvkCiQBHbGlcSHFcLzpIACIAWXgMeGY0XyJsHQAzfD4Mr3wAd0Z/anh8DGjIOUhsREp0AGs0RFgidRQMXApFbAZpdEh8kgEwASaooipuBAYU6tq+BJoKDLReyAAKAHEAVQBJAEpILL4BdK9aBKJ0DHpkjDRWjCuAZRrcbXuBSMgAERIpPLcNgdSCbfApZDrZ4/TaHXIFRAaGCwEiQZIJI4kajYfzIBIMLzYJT9WhUIhgTC0MCk/DcdpLFZrY5FLHiJ6VPIMeqIAA0kAkSGws382SUFEu/AyHPwXOQNQl2G4AHpaAUsKdcAQMLzrvQvjL6goMBl4GkqOIymFYF1sERYP1mIpZhcUvgiEiAEKcw24WTcPrOVG0WT3ZjwGY+eQXRBMKT+egZLrMK2oqyyXCwS36rl+EitRCdeTrL0Gmx5pCF/Z8/Lqe1MY2mnElS0xlHJZCkcjmqKJlgp6U5glNRAAcmQAAMAPqThLqafjnjONBsJbISlhP2ohAdRGQV7cbjZGh670b/14jAEomogAyN9ifIAYvAvEs+bsls2MHz1/gM5QdRSXxg1DcMgPkfx8yWOgNCMABBWgLgte5fAybBL2QlJ1HkfFCSUZAAE04NiG9IE7ZRkN1RcKEQS4+TIFQvAVeZkAIew0CkKi8jQBM8nyC9IAAKWeAB5AA5fg+CIkijmyZJcF3OBUzsRVaGoPp/zRD5IBsABROCfliXSNGYeh100wYBHpShGVRPSDKMgZa0gVUGEcdgv34LAQOXcMBx9TcXKQWNKHkdceNoZByF6RBEGcKMSH9G4yAYeANPwXMgngfxgQUjRcAAD00ZFUS8dSC0YWBMFIZBe2TTT0XVKhL3tQZ/Fkig1jYChSHoNj6hIRL+zDRJki8Yh0kqZqGHtDFEBDBhYIMaAQglBICwoHpkOQRB/VSjIIPQxJ2QsoZrOWVZUSqe4BSIEoToyzTx0smcOhsy6FwufwxGyMKbkgEgCtEPAJXHbh00zLA0APDRwfHJElPO2yUDXBgYtor5SplcD0FCWBNW4DglSVABGAAmAB2DQAAYaY0EmOAADmp5mkW2Fi0TeAHtRxVF/2y/zGDK2KA3ayCK0oKIawzSBxwAARLLkyyglwNHFgtKAXdcqs43haUoLx5F9f16CnV650nBduCXFcAI6lyQ18hgjRNM0vyW54GH8Mhmj/ZAlEdbVOnuq5NMWd61kDdoryUegMU02wXOkL34G4ZCkREuMgL5DNUETpQmLjNc0VnMbKjwI50LEUoUNBPZ7ehTYjj4llPPXO4HhIXL6IwKrL2YlQX2wq0bTtflBVmAh8C8HUfLDZ3s0NaMQpcPl61dptKNpbaf3+hwDyPCVXPcjo2/+pRuDyWRcvsEhcEVJb9GMcAoDIeh8ClNA8EIcju3oJhWDsC4LwfgwhRDiCkDIeQTAxQqDUJobQugwCGBMFAdweI1xQ2/sQMgFEogANylwKghQHBOBcJUaBihlCqHUFoHQT9n6mAMP4LKOV2CIkKpwAw0QeEGAsJAOCuwcFdnUvQUhyRyEf0qtVaQbgUSZXSGwncRwXyonyE0ao3A1LHjCBlCKYR8gZXIIUC+b8UppUQFwccuQmj1CtgkcgfUMpFxrpAAAzHTMmVEwayFkMuLw9iMCON0fyACriABsdMSZIjEhlQGFYj6JTMf3PobVUQ5XwFId+fBHQXBNDBIw5hLBwVfBRGurFHryKUEOc05TJQAwKoeCgOjsg8GwF8Py7B1AWKMLE8gsEeHRCMBAMARgXrhwur0JU9lDLGVMhwbhvD+GCOEXgsRjgJHyCkTNGRiB4KQGMTpfSsyVHfGZJpRukBHwtw9GcMUkAADqZ1dhvUmVuDRAhBpQ0QjBSAH4XIyhPgpfolkRhjHoKSB4FIbjUi6HSCZyNmSnBfBcdk7dMAPAlJc+E/EEbyJmY5fAeAmLkGQANeQGRWS8wvDHCUi9W6UQFGgFyZJoVUhpNwPkAoHDChIKKSgEppb2kVCqNU5dNRlB1PS9ejZamWgzKPe0uSIR8mkqRX+Z96DWxonRKi+8mmhGZMwbw4hL5XVPMbPoDgZroGQHeB8VyXxvj+Z+ZCfIyydDShIWYsQqFeF3vQbY4gmK+n8ruXSqx7T0stSjJOMZxSfPoEKlAwKZVu0xtRZcd87brl2qIeAJpnZoQwjXLC4hpBLXZp3doG0tp1KUO8F89h3jNIlJpa5tjb4UDjD+NG0haInXkQiy6fJkXnAlDiqBLKHiPCuOuNNOI6Xeh1BqtkVw1X8DwJ4XAAb2ICieEqbiqKriTttUJUSElWlqrxUGf4JBAQ31qNXS0TECysXkTQGaGBp5unkAWVY9QuAdsQIaUYXI358g9PgP2wc6QFlkCSogu7vWJB8MywSHE0Ce3FGne2EzVgQOwje05qJa1iF5rfZ9WAyDuW7MgRqJHMEQuyuA7IFj2gSGnlk9o/Y9YiDEEtOCChmC8BICibUkhURpHgMSS09ZPUajpfIXJBbZBLoNGePoqBcLXloEif5gNL4fHfdQQWlq1ztWXpkyWELHbzxxnVfsaYMxZlPOrQsGxxyK3qMrCsLhxyHHMiiAWC65VYFbF3DRUcEiUooPGI4SZ+xm1nLgecmbbY0XqViG10rhaKXkdJ4kK9kCZkKGxfRhzo2binV7VkEp1zuYFeyQ5Q4RY6hUwdBpCT2RlD6FUVSIcjRkYIDRXdGTOJVfPBQ6oGBGvih63wAud8JStdqLuD0IQeBUGrjFe2MbtwKT5EoFYJr+QpGCLPQFuUvy7poAWer7U9YCljkiOC1GCrLjNfUiK9WDkkEKNEH41BmXQGaogY4yZvMVGTd68URLkDaqzauWNEIMo6Ze38oOm0qO1VadDS+an2Q7KeH1iCIRnBrv7PMOMVE7uU6jq0KQGAlpFIEaUsLFT+zVLKhzrLjSjxRFaZ4Dpzsunlr2QYKAfTUTrHQdIkn6BELF0OQS3SAK3LXcoiab4QqiWhBgbempX5ET7EWUMyXTDxmvNskqfHsNZALMGXw4pQjV1RHEfFepxOar7MOcBw0ll0AHiYjMSi2mRORFylENiT2ZOomZZC8klJYW0gGGdYdTJWljuPVRDus69wwmbgifToQ55gUjGRXBdGzOBWOylupVRFhdDzdXQd6Tywa3i7l4c0gRxDc6FtbInPAbBzEC7WVnkIvtj5OjiUiOMuadniQE7r5AL/RYYonsZUiD5fSdB0INoaC5mSplhq2kM98y7kZo/yb/y1Br+eJaiNxy2+4PAJUi9EALjfoeRYuZ74UDahc52bl6GyV4iI6KTa3omiJDbRnrCTiTEYv7Qzv5u6Th+LMABLWAiTPDQDcy0C/4girC9BpzFwIESQ4rj5uxupGhxjArZZRqniLB6KQBqpgAALWziAZoXBUae7JrjiLyTiWpaz/QCHeiThpoLj5LnA6iibU4SjbylqoRpQyGQBMQDSywcqTi0CKgh7lTjhjZ3w4hAGaTzDwBYQABeUQG6G0lwSBr+7+2qtQ6B/iC4VgOBeBP++Af+xBiUwKG68m7AfIThWmoQzB/Qr6oQUin+XKWE2ifWFq1W6Ad060oQmk9Q36+Q4Wd8u69YdB76Xc9IlSai6e1ul0Y4564kHU8kTkMs9KuwPwOooY0ga8ZQrsa+Wq+AtEcBzgzUvUkAmY4oFhZQ7wkYJe/+xhyAlAXQmWRqSAA6Vw2iaAM+GA3qTE9AaqfIrS6EGRYqUBiIci8eB40ceEfQ0xrSfctAiGe2V+ZUR+/gJ+uqG6Thuq64GqvggMJBO8ExgB9WB4cK4o6kkAAAEtANAFYM2tiMgAbkxgDHFq0mwLFJ3LvlcsLIaPjn4OhMgJaAapAMzMzLUfaEoKcFcAxJjHpqziUq6nUmxJpNzkuHAVIoZgLtkm0iLtzOED0pLpAGJP9qiZ2hiakZgGsOOPjguLVqIjxs9EMK9AyJdK/twPbvDNybyYUMWlRrLKQKloIeZpOHVJOO5i4OsPsFrD8k4g7KBBGGAW7g/qRjNJFrVIlppPSoabILBFLnyYfu8jcJEBQFYsdu0EoAVMaVrKEC/guPIW3lpBiBfmELcepO6TyZ6Xrt6VcZQP6cvpXtqcuiGbjLLA4R/suguP4AAcdFcPsZUWJImaqX4CmQMZSL6RmVKGgRgV4LmaZsgW/kqC2a4egH2mnBKO4bgZWUXiYsDlRCWcYRKNeiqcmXgKmY2ZoZmSES4Zge2WGQWSua2VrP2ZTkOXgQESMewBOUYb8eyOQaOS5MDgMrwhbqMgYFbnKVMhvixrlBwkVI7ksi7qsn/PYBsp7tsn3D7gYEJoci+UosCtrqiOcvIoHrGVVDIF8orrHHyC3qpm2taINNYBDDiQBtWqYslCksgMmoDARqEjqpKkBmie4nTNTHyGmBupErTCTLuv7mAA8tAI+JANEmTPTHiqgFBbGuHMlOaXXuQP2NFP2p7tbFyMiXtrBaUU+aiGQAKF0BgLlCzs7mztSZKiEnSaIDzsbnzgaoLnwMLiHhyd0rIgYNLmbsMpbjKRfkqP+uIAwEqAwLFM5b6JEBoB5ZYnZcsq7lXpKR7pIlKN7tZaBXyQAMLPDPDNoIbSAoh3xCUdBdA6G9AQrL6zjsjwaRC458DMqLxgq1CBq7DCaHjkAgjzQazMDEZ5VaaXinH9DyYfB1DwzFWtUJCayElXnNBRACD8qjaLiISvH/S5CAxqFoCyB672x0Hl6Qlcj1Z7wyUKHhUIDnBhpIgABqE8vgLlfQzIiebKKe8KryBGdSUcDVSazk8Mx11mFA4peWGkVU+uQFV0Q1VEYG9QEwRK/0TAeQNE+mNwkgMmQovgn+ssGgi8C4OsaiCAR+31v16EkUqF7SEQH1FAYoOozQPECIwRP2TwfIJoQMN1tAGYu6I0SQjgAxJApo+M1YzkmYNOua964C7u7wd2VopmPEQgXMuNqo/EX1n1ANQ+SIPwZI+eB1O0EY8e41PE5qBoxFzkbN3qOiFhYACQQZVEZAfcmV9gVUgt62y6jAmABtBaoQ81VpUYi67I91NOt+YOEOYQYOdx5Sgm9gXs08ZUGaHV3oYAkR4pIxbVfATEYYwKwWdooQ64ZJ+V5FrlswD1lB8a3tuqyakN60ceCVkQ5p2uvgNY5N9o2s9NTEUdXkAxD1nop4n1WtYEI2E40N/tQ1Wsj2XR3SvW5pmkkQGQURUokN6dAGyN/1P6fAEV/sttixktdhvwcKgZddQ+UNHKmtINoe2QLd8eAgiA0885YBh43RNcUQgx8Awxp8ENzgJm0dB4WIhVg1nEWe96BQYaLkE906Vw+sHOjNMswcEmPR/0F1nE0t8u+ekd+MVEotFAgmo1mEvg9cbNb5Jx14UNxVlxkQC44Yule97dwSrmSt9sie9ZPpqSTND1VEie+e4DQDLRSD/tF1kqQdp83VOQWeeAWoa4pdwS4oZdyau0qwY1psjdBo7BR5HQUhrSVtsw/6S1PWfdp4gRHQCMI+2da0TV14DdxVzRC4yaWQIIgw9N0dK1vDRN1DgjwhvVxqOiFDuaY0vpskHQu6ftgjzRYAaxwQmjzk/6NwzgfUij4DS0mcygPgOc8iDV/VKVse+EFV/g4mtEAD3lE6eOr9d0mMqFbNYgjEitOYsZMOe1mIdA319sNQdQ+GOOeqA+5G8WcjoQchHk6chSWlVJZSultJVSBlDJdSTJ/OzSplbJFlYuXJHphQsV8Vq2AYZpUpj5EcJAXlJQblflXliVvlsU44CykAugxjXIQjDDXYBh6z9QmzjaXYGg9tmsBgqzUADjXIOzFzgGzNmsfI1zGgtQkQYgdAVzAjXIRzU9Twbzi8nzUKD1Pz3ofz5ID1FIt9JA44pzaz1zAdFYgLgjkRkA7zdi9zyLsLFUyLHAtzj1qLxViLyLKAkLZzuzYAzduLQLZDEoaLzdUL5znztIy9PB1Aa9qLS9tdq9EDKg1mbzbLK9zLEDnyfEPLcKjL89nL1cUgRL0LaLKDEL5LgjsrWLAL8rHzlL7IMrDZELtLJLtD2o8LGzurMgLDZQUrdLnVwjuAprJLGjKrezwhtrYATjLjWrTud5YyjlZRUyLl4YSoQgiACpGgfrn5QygVP5IV/5YVlDEucuhyGG3q2GqchqqiKAEeXc7CJIrKyeHKSMl09seeK2bRE+GakNlozKMwqxTQ4x9elhCRGm5mCWfYphlALN/0jUBzmC50tciL0RJtt+wWfA5mrRDY1B5Su6mD20VduD5blQQYktPYiWzK1skQmoqIIxxRptFbOo+QKI7U/4YU7UQpHQ5hOiya6E8AQQqIDRsJx2X8r4VBm87thxCgxI+qR4/sXz5ZvbtY8c8i5b3qnOIoWNdQ/4zAn99o3K4NLKqe7L/LO0pWyj8wlEhBwKXLd9JwD9ZWzgWpW1M9UKWbs9d6IIsrmWeefMVSZIahBaogsgBIJAKxtKdtFH/6zSR2FHD1rHDwHJlAHHVwkQHEdHpDcKfIA2lOzpJtvR01fZTAWNlwYBDWJAYADxYoGFqIf7lbRgEaOWsjZtny978WTKDoTowE5heQocr1FpTsOMbuxcztowpV5dYndbm4gAmATQmFsjstj2nJBo0Hytrsiw6lA1DtDbqL5/k2oaI0BFR8gYCOCfIUA/hxaSfrAEJoBgC1CI7HhVgjmCD8aHY60xfdTG7EahaeTO1rEyblQKIsYJgqGRRUQicvUZP1Cud6eeQtj8dLTebQk6eoiOjcaKiSQApZE8Zd0VjjxFXGvah8jjZ0BbGj3ue8x6YlTH7KfsjqmUT+DZ4FEbv/u/AiSxAAyR7ptsT+ATVj6aRuRxbHmQ0SeyD0d4RHwUeytSqJH+hUTNFBS7RlR3dUS6viqsNP7yKUE9tTs9d7oDV/pvwqeMA4gPH64LdhYhJFUAZvwCJWDlUZimbWf9Abr+DVOnw0HbvhizQFHeRqhHpRBRYkZIjPBJAvjOCGyqrESkTTu6du5mTnxIBGayBRA/uoj1ytIU8JiJaFMLAYDboE00RRAGdm1wTo+7qFPmm4gjZkdNc7Sc0kBLS6Twl8AoNrR4iptR4JitJy/lVWnK0yyi/oC+kofD3ajTxYxECzrFdUJzB9BQYwaTBohGcFWteUTrgboJAhcuimewmPgjFwSDBb1sAoD1i++wNHc7hLTLLs5GXNOG6GWMlSjMldOsnmWdJHvi7DJJmFBEB5CjCoRVw9FjMYjSkCCymTPTOuW+v+t25+vLPavjiysS0PDPAtq4DrAkAmmd/d9kj+MUCD/D/Etd+as99EC6SvKT9Wsz+ENz83hYhSBL8j+z9wpb/T+yveZz998U57/QsH/ehz8L+0Cn/nPn8Ghz/j83+yx39ciX+L9D/L8v/1Br8b8kBP8r9plD+u/D/p3xm7eZ1gMmKftC1CwkBwBkA5fnFCkDeZoq7nEMp30QEkBrkBQNAdPzdxqocB0LI9PgKgHnMQixA5fn11gHeh1gi8XYLQCOwsYqMJA2WEp0oBdc0BrrEZO63r5OUaAEecqEqC1qAwNA+MTAsG3qZBUIC7uCNlsnWq7JfcfJUEjJD4HX5oKybccEIIKgiDcAmBNBheE6DdB9aMFVEJciwGFBbkKKOoHBU9aa8XUbvZAB7wUiwZIAAAViojh8QQkfaQCwGgqtI8qy1OOHH2IzWxSAU1GahXDj4VhsSn8ewHHlGB8B5U8ibuqXi57fdHuM6dJjEDgjep6eGaLrhUDLzW1wCayBtsmGZRiEDQQharJ9HDCYRyE1sEWML0bbyJvqb8YjAxkqboBigDneRJw1AZdUgCzKaIGYMgDRVMA/7CoDMCxoDAd2avDdjOyg5X0OeZkRLiTnoG9UNQrDEJBgP7A4o+QR6Vgsz2C54Bd0NpDdHrm3SegMO/YNThghapeMDaBQaHuz0OEyQMQi7U7nJGoA6JIu0dYoCsW6Q4wEAPyZnG4EMSQBHBG0aGN7zUgzwuhqIdHHQC4C9Z7YoWZRhcGeyQd+6zkBPCASLRyRd0mAP8Du3tgND6c/0I9NOSOGUElinoYkTCIhD1lOI+OAEsey3BYhsao6RQJQjeh2MqIWQP8HUE2G6VmQ5bXoNcT8EcRLgh6cEDnkNZLQ42WGFOLhnDgTBPIiIhHMOGh4VCuQVQ88J9HHJU8LyOXcBHpSB5qUaAf9CkbKOWoNIlgtceNIm1lht8v8qIpiOwDS5Z1TO4YOkZ72hgVFFRCbXDKcEIapUY4PYBEkZyoj1x1uShIjMtxUF3FSoCQbkEjxh4Fg+wQzEjPbB+zQMlGTwPxlnECb9hExQJWoMCgG4J5M2MKbNjGOYYooJ0fER0n2FExiUBqy6IdhvHmxmcu4u6HPmukEEiZD47IXHolFxA1NH2hSFPjpRMLrt6SvODpiZTz7tJemhffpiX2zERQkRssCZm8iVCli7sgg5KFoNEFYExK0PBjEoNIgHjfBA7eRKYJuR3I6gewG8ubi4EPkPWSlfcSEEnA6lqs9ucQaGxeGhVZBUbBQWVk2yOiVR+g9Kj0GjzOIBUnWC7tkG+i4BSU0Q20n73rQCpuMjmZlK6XqTd42s9VZUUmxohVN5swKTSMQiNCWjgUUiRzjmFdL7Dusc6HwLmCgg2ZBYIzXcP8n/BYBze/QWMfKgygllxQd6W0mwzKBXA+2dNebsOwfaec2wUWf6LwEWAFF787w6ot8KiAXkGqJEnDFUxshyRi4kQjWJeGgQogcwVQDgs4Gh7koBIDBfyBURK6UQp8zKB4MZjSLyJ582aPgM0QwmMTDQq2CosllgJpYVwmYegIPwKgMcrg44J5hkBEKmxBCGjfYFWyUBHt8ksJPHm+2uDKZ5iChPgCPioCQAHJI3X9gj08j+A3anEdPv3mxwjZ0sfklElBKiJbo8A/sO+NoBzrtAIcpXVpE5KgJgMyg4Yi3kXT/JkIcIfLSnNuzvgkigIWE3SvJBtT8w+AuIPaBAlU55Z2ggIpiFYSBrLc2psafCKaGCQnd0IkJTxnkHErrhwmfQDglEx9iSYhsDvTdLgBC5GpMAkHdaJtJriaVpxjTWcVzlaYLjs+nTFpGZRXEF9OS1lAZpXBLSlszSViOnL+PELmZcytfXcTblRl/jH8cMOyhbkMAGBUE+Bb7NgheEEIgEfgNACQhkHTYDcVAGhAgnoTIISZL8YTOHRnCRQDS/wNKIMFoCThmOoQBhKTLoBkwBAJMRmC4LQAAAWHiBTEZhuJZZssgAJzhJGYakVWbQDRiyyXBDAWWYzApgUxZZFMAQG4gEAMA1ISCFBBzJcG0ABAqs1WSTBIAuDzZ4SNAC7JJgCAVgEsmWWgAyCyySY4SNSI7LJgMAKYLg6mHQBIAkwbZ7MiAJAHCTaySYPEAQIzGdkuCnZLg2WRkAph0AGAZMNxMvloCMwyYGQVQKrOpgCBqYas0YGTFlnxzSZqshgG4iVluIMgiEUuarLVmqyVgsstxDLMZg1ySYqs2OWTDlkSyjZbiNADnOdiiyOZBCOcDJkQC8yBQ/2OgJOFR76AgAA== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Red-Hat-AI-Innovation-Team/sdg_hub&utm_content=90):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-23T20:43:07Z
body: ## [Codecov](https://app.codecov.io/gh/Red-Hat-AI-Innovation-Team/sdg_hub/pull/90?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Red-Hat-AI-Innovation-Team) Report
All modified and coverable lines are covered by tests :white_check_mark:

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: shivchander
created_at: 2025-05-25T01:17:28Z
body: Thanks for this PR! 😎 
author_association: COLLABORATOR