<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Project AI v1</title>

  <!-- Google Fonts for a more sci-fi look -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link 
    rel="stylesheet" 
    href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap"
  >
  
  <!-- jsPlumb (Community Edition) -->
  <script src="https://cdn.jsdelivr.net/npm/jsplumb@2.15.6/dist/js/jsplumb.min.js"></script>

  <style>
    /* Reset & Base */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    html, body {
      width: 100%;
      height: 100%;
      font-family: 'Share Tech Mono', monospace;
      background: #0b0b15; /* Dark background */
      color: #e0e0e0; /* Light text for contrast */
    }

    /* Container for the sidebar + main content */
    .container {
      display: flex;
      width: 100%;
      height: 100%;
    }

    /* Sidebar (Toolbox) */
    .sidebar {
      width: 220px;
      background: #161626;
      border-right: 1px solid #222;
      padding: 10px;
      box-sizing: border-box;
      overflow-y: auto;
    }
    .sidebar h2 {
      font-size: 16px;
      margin-bottom: 8px;
      color: #ffffff;
      border-bottom: 1px solid #444;
      padding-bottom: 5px;
    }
    /* Category Header Styling */
    .sidebar .category-header {
      font-size: 14px;
      margin-top: 15px;
      margin-bottom: 8px;
      color: #b8b8ff;
      border-bottom: 1px solid #444;
      padding-bottom: 3px;
    }
    .sidebar button {
      display: block;
      width: 100%;
      background: #4b0082; /* Purple */
      color: #fff;
      border: none;
      border-radius: 20px;
      padding: 8px 20px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s ease-in-out, transform 0.2s ease-in-out;
      margin-bottom: 8px;
    }
    .sidebar button:hover {
      background: #662d91;
      transform: translateY(-2px);
    }

    /* Main content area */
    .main-content {
      flex: 1; 
      display: flex;
      flex-direction: column;
      height: 100vh;
      overflow: auto; /* we'll manage scrolling in canvas/log if needed */
    }

    /* Toolbar (header) for main actions, top of main content */
    .toolbar {
      width: 100%;
      height: 60px;
      background: linear-gradient(to right, #2c026c, #4b0082);
      border-bottom: 1px solid #222;
      padding: 8px 15px;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .toolbar button {
      background: #4b0082; /* Match the toolbar's purple tone */
      color: #fff;
      border: none;
      border-radius: 20px;
      padding: 8px 20px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s ease-in-out, transform 0.2s ease-in-out;
    }
    .toolbar button:hover {
      background: #662d91; /* Slightly lighter purple for hover */
      transform: translateY(-2px);
    }

    /* Toggle Switch Styling */
    .toggle-container {
      display: flex;
      align-items: center;
      gap: 5px;
      margin-left: auto; /* push to the far right in the toolbar */
    }
    .toggle-container label {
      font-size: 14px;
      cursor: pointer;
    }
    .switch {
      position: relative;
      display: inline-block;
      width: 50px;
      height: 24px;
    }
    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: 0.4s;
      border-radius: 24px;
    }
    .slider:before {
      position: absolute;
      content: "";
      height: 18px;
      width: 18px;
      left: 3px;
      bottom: 3px;
      background-color: white;
      transition: 0.4s;
      border-radius: 50%;
    }
    input:checked + .slider {
      background-color: #7e00ff;
    }
    input:checked + .slider:before {
      transform: translateX(26px);
    }

    /* Connection Status Indicator */
    .connection-status {
      display: flex;
      align-items: center;
      gap: 5px;
      margin-left: 15px;
    }
    .status-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background-color: red; /* Default to disconnected */
      transition: background-color 0.3s ease;
    }
    .status-connected {
      background-color: green;
    }

    /* Feedback (messages for errors/success/info) */
    .feedback {
      display: block;  
      margin: 8px 15px;
      padding: 8px;
      border-left: 3px solid #4CFF4C; /* Green for success */
      border-radius: 4px;
      background: #232336;
      font-weight: bold;
      color: #4CFF4C; /* Green text for success */
    }

    /* Canvas Container for Pan and Zoom */
    .canvas-container {
      position: relative;
      flex: 1;
      background: #101020; /* Dark sci-fi like */
      overflow: hidden; /* Hide scrollbars */
      cursor: grab; /* Indicate draggable area */
      /* Removed transform-origin to prevent frame movement */
    }

    /* Change cursor when panning */
    .canvas-container.panning {
      cursor: grabbing;
    }

    /* Main canvas area */
    #canvas {
      position: absolute; /* Positioned absolutely within the container */
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      overflow: visible; /* Allow jsPlumb to manage connections */
      padding: 10px;
      transform-origin: 0 0; /* Set origin for transformations */
      /* Initial transform */
      transform: translate(0px, 0px) scale(1);
      /* Ensure the canvas can grow beyond its initial size */
      min-width: 100%;
      min-height: 100%;
    }

    /* Log Area Styling */
    .log-area {
      width: 100%;
      height: 200px; /* Adjust as needed */
      background: #1e1e2a;
      color: #e0e0e0;
      overflow-y: auto;
      padding: 10px;
      font-family: monospace;
      font-size: 12px;
      border-top: 1px solid #444;
      /* Ensure the log area uses Flexbox for better layout */
      display: flex;
      flex-direction: column;
    }

    /* Block (node) styling */
    .block {
      width: 240px;
      min-height: 110px;
      background: linear-gradient(160deg, #1e1e2a, #232336);
      border: 1px solid #2a2a3b;
      border-left: 6px solid #7e00ff; /* Sci-fi color band on the left */
      border-radius: 6px;
      position: absolute;
      padding: 10px;
      cursor: move;
      box-shadow: 0 2px 10px rgba(0,0,0,0.35);
      transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
      z-index: 1; /* Keep the block below endpoints */
      color: #e0e0e0;
    }
    .block:active {
      transform: scale(1.01);
      box-shadow: 0 5px 15px rgba(0,0,0,0.45);
    }
    .block-header {
      font-size: 15px;
      font-weight: 600;
      margin-bottom: 8px;
      color: #f0f0f0;
      position: relative;
    }
    .block-header .delete-block {
      float: right;
      color: #ff4c4c;
      cursor: pointer;
      font-weight: bold;
    }
    .inputs-section,
    .settings-section {
      margin-bottom: 8px;
    }
    .inputs-section h4,
    .settings-section h4 {
      margin: 4px 0;
      font-size: 12px;
      font-weight: bold;
      color: #b8b8ff; 
      border-left: 3px solid #7e00ff;
      padding-left: 5px;
      margin-bottom: 5px;
    }
    .inputs-section label,
    .settings-section label {
      display: block;
      font-size: 12px;
      margin-bottom: 3px;
      color: #e0e0e0;
    }
    .inputs-section input,
    .settings-section input {
      width: 100%;
      padding: 5px 8px;
      margin-bottom: 5px;
      border: 1px solid #444;
      background: #20203a;
      color: #e0e0e0;
      border-radius: 4px;
      font-size: 12px;
    }

    /* Hidden class for text fields (when connected) */
    .hidden {
      display: none;
    }

    /* Endpoint label styling (arrows/bubbles) */
    .endpointLabel {
      background: #7e00ff;
      color: #f0f0f0 !important;
      border-radius: 3px;
      padding: 2px 4px;
      font-size: 10px;
      font-weight: 600;
      pointer-events: none; /* Labels will not capture mouse events */
      white-space: nowrap; /* Prevent text from wrapping */
      font-family: 'Share Tech Mono', monospace !important ;
    }
    .endpointLabel.leftLabel {
      text-align: right;
    }
    .endpointLabel.rightLabel {
      text-align: left;
    }

    /*
      Ensure jsPlumb endpoints and connectors
      appear on top of the blocks
    */
    ._jsPlumb_endpoint,
    .jsplumb-endpoint {
      z-index: 9999 !important;
      pointer-events: all !important; /* Ensure endpoints are interactive */
    }
    ._jsPlumb_connector,
    .jsplumb-connector {
      z-index: 9998 !important;
      pointer-events: none;
    }
    ._jsPlumb_overlay,
    .jsplumb-overlay {
      z-index: 10000 !important;
      pointer-events: none;
    }

    /* Enhanced CSS for Workflow Result */
    .workflow-result {
      background-color: #2a2a3b; /* Darker background for distinction */
      border-left: 4px solid #00ff7f; /* Green accent */
      padding: 10px;
      margin-bottom: 5px;
      border-radius: 4px;
      white-space: pre-wrap; /* Preserve line breaks */
      word-break: break-word; /* Handle long words or URLs */
      width: 100%; /* Ensure it spans the full width of the log area */
      font-style: italic;
      color: #00ff7f; /* Green text */
      overflow-wrap: break-word; /* Additional wrapping */
    }

    /* Additional Styling for Settings Toggle Switches */
    .settings-section .switch {
      width: 40px;
      height: 20px;
    }
    .settings-section .slider:before {
      height: 16px;
      width: 16px;
      left: 2px;
      bottom: 2px;
      transition: transform 0.4s;
    }
    .settings-section input:checked + .slider {
      background-color: #7e00ff;
    }
    .settings-section input:checked + .slider:before {
      transform: translateX(20px);
    }
  </style>
</head>
<body>

<div class="container">
  <!-- SIDEBAR (Toolbox) -->
  <div class="sidebar">
    <h2>Toolbox</h2>
    <!-- We dynamically insert categories and buttons here -->
    <div id="toolbox"></div>
  </div>

  <!-- MAIN CONTENT AREA -->
  <div class="main-content">
    <!-- Toolbar at the top -->
    <div class="toolbar">
      <button id="runWorkflowBtn">Run Workflow</button>
      <button id="exportJSONBtn">Export JSON</button>
      <button id="zoomInBtn">Zoom In</button>
      <button id="zoomOutBtn">Zoom Out</button>
      <button id="resetZoomBtn">Reset Zoom</button>

      <!-- Toggle Container for Disable Validation -->
      <div class="toggle-container">
        <label for="disableValidation">Disable Validation</label>
        <label class="switch">
          <input type="checkbox" id="disableValidation">
          <span class="slider"></span>
        </label>
      </div>

      <!-- Connection Status Indicator -->
      <div class="connection-status">
        <div id="statusDot" class="status-dot"></div>
        <span id="statusText">Disconnected</span>
      </div>
    </div>
    
    <!-- Feedback area (no alerts, we use this instead) -->
    <div id="feedback" class="feedback">Welcome to Project AI v1</div>

    <!-- Canvas Container for Pan and Zoom -->
    <div class="canvas-container">
      <!-- Main Canvas for Blocks -->
      <div id="canvas"></div>
    </div>

    <!-- Log Area for Live Logs -->
    <div id="logArea" class="log-area"></div>
  </div>
</div>

<script>
  
  /*
   * Block definitions for your drag-and-drop builder
   */
  const blockDefinitions = {
  "analyzeWeb": {
    "displayName": "Analyze Web Page",
    "inputs": [
      "HTML"
    ],
    "inputTypes": [
      "Text"
    ],
    "settings": [
      "Advanced"
    ],
    "settingsTypes": [
      "Boolean"
    ],
    "settingDefaults": [
      false
    ],
    "outputs": [
      "Analysis"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "AI"
  },
  "compare": {
    "displayName": "Compare Texts",
    "inputs": [
      "Text 1",
      "Text 2"
    ],
    "inputTypes": [
      "Text",
      "Text"
    ],
    "settings": [
      "Advanced"
    ],
    "settingsTypes": [
      "Boolean"
    ],
    "settingDefaults": [
      false
    ],
    "outputs": [
      "Compare"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "AI"
  },
  "extractData": {
    "displayName": "Extract Page Data",
    "inputs": [
      "HTML",
      "Search"
    ],
    "inputTypes": [
      "Text",
      "Text"
    ],
    "settings": [],
    "settingsTypes": [],
    "settingDefaults": [],
    "outputs": [
      "Data"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "AI"
  },
  "getWebPage": {
    "displayName": "Fetch Web Page",
    "inputs": [
      "Site URL"
    ],
    "inputTypes": [
      "Text"
    ],
    "settings": [
      "Render"
    ],
    "settingsTypes": [
      "Boolean"
    ],
    "settingDefaults": [
      true
    ],
    "outputs": [
      "HTML"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "Data"
  },
  "loadGDoc": {
    "displayName": "Google Doc Import",
    "inputs": [
      " Doc ID"
    ],
    "inputTypes": [
      "Text"
    ],
    "settings": [],
    "settingsTypes": [],
    "settingDefaults": [],
    "outputs": [
      "Text"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "Data"
  },
  "output": {
    "displayName": "Output",
    "inputs": [
      "Output"
    ],
    "inputTypes": [
      "Any"
    ],
    "settings": [],
    "settingsTypes": [],
    "settingDefaults": [],
    "outputs": [],
    "outputTypes": [
      "None"
    ],
    "category": "Utility"
  },
  "summarize": {
    "displayName": "Summarize Text",
    "inputs": [
      "Text"
    ],
    "inputTypes": [
      "Text"
    ],
    "settings": [
      "Advanced"
    ],
    "settingsTypes": [
      "Boolean"
    ],
    "settingDefaults": [
      false
    ],
    "outputs": [
      "Summarize"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "AI"
  },
  "textInput": {
    "displayName": "Text Input",
    "inputs": [
      "Text"
    ],
    "inputTypes": [
      "Text"
    ],
    "settings": [],
    "settingsTypes": [],
    "settingDefaults": [],
    "outputs": [
      "Text"
    ],
    "outputTypes": [
      "Text"
    ],
    "category": "Utility"
  }
}

  /**
   * Dynamically create toolbox buttons grouped by categories from blockDefinitions
   */
  window.onload = function() {
    // Grab the toolbox container
    const toolboxContainer = document.getElementById('toolbox');

    // Group blocks by category
    const categories = {};

    Object.entries(blockDefinitions).forEach(([blockType, blockDef]) => {
      const category = blockDef.category || "Uncategorized";
      if (!categories[category]) {
        categories[category] = [];
      }
      categories[category].push({ blockType, blockDef });
    });

    // Sort categories alphabetically
    const sortedCategories = Object.keys(categories).sort();

    // Create a section for each category
    sortedCategories.forEach(category => {
      // Create and append category header
      const categoryHeader = document.createElement('div');
      categoryHeader.className = 'category-header';
      categoryHeader.textContent = category;
      toolboxContainer.appendChild(categoryHeader);

      // Create and append buttons for each block in the category
      categories[category].forEach(({ blockType, blockDef }) => {
        const btn = document.createElement('button');
        btn.textContent = blockDef.displayName;
        btn.onclick = () => createBlock(blockType);
        toolboxContainer.appendChild(btn);
      });
    });

    // Now that the toolbox is created, proceed with jsPlumb initialization
    jsPlumbInstance = jsPlumb.getInstance({
      // Larger endpoints with an outline:
      Endpoint: ["Dot", { radius: 10 }], // big "bubble"
      EndpointStyle: {
        fill: "#7e00ff",   // Neon purple
        outlineStroke: "#ffffff",
        outlineWidth: 2
      },
      Connector: ["Bezier", { curviness: 50 }],
      PaintStyle: {
        stroke: "#7e00ff",
        strokeWidth: 2
      },
      HoverPaintStyle: {
        stroke: "#db2aff",
        strokeWidth: 2
      },
      EndpointHoverStyle: {
        fill: "#db2aff",
        outlineStroke: "#db2aff"
      },
      ConnectionOverlays: [
        ["Arrow", { width: 12, length: 12, location: 1, id: "arrow" }]
      ]
    });

    // Bind connection events
    jsPlumbInstance.bind("connection", function(info) {
      connections.push({
        sourceId: info.sourceId,
        targetId: info.targetId,
        targetEndpointUuid: info.targetEndpoint.getUuid()
      });
      // Hide/disable the matching text field
      toggleTextFieldForConnection(info.targetEndpoint, true);
    });

    jsPlumbInstance.bind("connectionDetached", function(info) {
      const idx = connections.findIndex(c =>
        c.sourceId === info.sourceId &&
        c.targetId === info.targetId &&
        c.targetEndpointUuid === info.targetEndpoint.getUuid()
      );
      if (idx !== -1) connections.splice(idx, 1);

      // Show/re-enable the matching text field again
      toggleTextFieldForConnection(info.targetEndpoint, false);
    });

    // Initialize Server-Sent Events
    initSSE();

    // Attach event listeners for Run Workflow, Export JSON, Zoom In, Zoom Out, Reset Zoom
    document.getElementById('runWorkflowBtn').addEventListener('click', runWorkflow);
    document.getElementById('exportJSONBtn').addEventListener('click', exportJSON);
    document.getElementById('zoomInBtn').addEventListener('click', zoomIn);
    document.getElementById('zoomOutBtn').addEventListener('click', zoomOut);
    document.getElementById('resetZoomBtn').addEventListener('click', resetZoom);
  };

  let jsPlumbInstance;
  let blockCounter = 0;

  // We'll track each block as: { id, type }
  const blocksData = [];
  // We'll track connections as: { sourceId, targetId, targetEndpointUuid }
  const connections = [];

  /*****************************************************
   * Helper to show feedback messages
   *****************************************************/
  function showFeedback(message, type = "error") {
    const feedbackEl = document.getElementById('feedback');
    
    // If message is empty or null, keep the feedback box as is
    if (!message) {
      return;
    }

    // Otherwise, update it
    feedbackEl.textContent = message;
    
    // Reset previous styles
    feedbackEl.style.color = "#e0e0e0";
    feedbackEl.style.borderLeftColor = "#ff4c4c";
    
    // Change color/border for error vs success vs info
    if (type === "error") {
      feedbackEl.style.color = "#ff4c4c";
      feedbackEl.style.borderLeftColor = "#ff4c4c";
    } else if (type === "success") {
      feedbackEl.style.color = "#4CFF4C"; 
      feedbackEl.style.borderLeftColor = "#4CFF4C";
    } else if (type === "info") {
      feedbackEl.style.color = "#4C4CFF"; 
      feedbackEl.style.borderLeftColor = "#4C4CFF";
    }
  }

  /*****************************************************
   * Create a Block Based on blockDefinitions
   *****************************************************/
  function createBlock(blockType) {
    const def = blockDefinitions[blockType];
    if (!def) {
      showFeedback(`No definition found for block type: ${blockType}`, "error");
      return;
    }

    blockCounter++;
    const blockId = `block_${blockCounter}`;

    // Create the block element
    const blockEl = document.createElement('div');
    blockEl.className = 'block';
    blockEl.id = blockId;
    // Position so they don't overlap exactly
    blockEl.style.left = (50 + 30 * blockCounter) + 'px';
    blockEl.style.top = (50 + 30 * blockCounter) + 'px';

    // Build the HTML from the definition
    let html = `
      <div class="block-header">
        ${def.displayName}
        <span class="delete-block">X</span>
      </div>
      <input type="hidden" class="blockType" value="${blockType}" />
    `;

    // If there are inputs, create an Inputs section
    if (def.inputs && def.inputs.length > 0) {
      html += '<div class="inputs-section"><h4>Inputs</h4>';
      def.inputs.forEach((label, i) => {
        const typeLabel = def.inputTypes && def.inputTypes[i] 
          ? def.inputTypes[i] 
          : "any";
        html += `
          <label>${label} [${typeLabel}]:</label>
          <input type="text" data-input-index="${i}" data-input-label="${label}" class="inputField" />
        `;
      });
      html += '</div>';
    }

    // If there are settings, create a Settings section
    if (def.settings && def.settings.length > 0) {
      html += '<div class="settings-section"><h4>Settings</h4>';
      def.settings.forEach((settingName, i) => {
        const settingType = def.settingsTypes && def.settingsTypes[i]
          ? def.settingsTypes[i]
          : "any";
        const defaultValue = (def.settingDefaults && def.settingDefaults[i] !== undefined) 
          ? def.settingDefaults[i]
          : "";
        
        // Determine input type based on settingType
        let inputElement = '';
        if (settingType.toLowerCase() === "boolean") {
          inputElement = `
            <label>${settingName} [${settingType}]:</label>
            <label class="switch">
              <input type="checkbox" data-setting-name="${settingName}" class="settingField" ${defaultValue ? 'checked' : ''} />
              <span class="slider"></span>
            </label>
          `;
        } else {
          inputElement = `
            <label>${settingName} [${settingType}]:</label>
            <input type="text" data-setting-name="${settingName}" class="settingField" value="${defaultValue}" />
          `;
        }

        html += inputElement;
      });
      html += '</div>';
    }

    blockEl.innerHTML = html;
    document.getElementById('canvas').appendChild(blockEl);

    // Track in blocksData
    blocksData.push({ id: blockId, type: blockType });

    // Make block draggable (only if jsPlumb is already available)
    if (jsPlumbInstance) {
      jsPlumbInstance.draggable(blockEl);
    }


    /******************************************************
     * Create multiple input endpoints (left) if needed
     ******************************************************/
    if (blockType !== "textInput" && def.inputs && def.inputs.length > 0 && jsPlumbInstance) {
      const inputCount = def.inputs.length;
      def.inputs.forEach((label, i) => {
        const endpointUuid = `${blockId}-in-${i}`;
        const yPos = (i + 1) / (inputCount + 1);

        jsPlumbInstance.addEndpoint(blockId, {
          anchor: [0, yPos, -1, 0],
          uuid: endpointUuid,
          maxConnections: 1 // typically 1 for an input
        }, {
          isTarget: true,
          overlays: [
            ["Label", {
              label: label,
              location: [-1.5, 1.5], /* Move label further left */
              cssClass: "endpointLabel leftLabel",
              labelStyle: {
                cssClass: 'endpointLabel',
                css: {
                  'text-align': 'right',
                  'white-space': 'nowrap'
                }
              }
            }]
          ]
        });
      });
    }

    /******************************************************
     * Create multiple output endpoints (right)
     ******************************************************/
    if (def.outputs && def.outputs.length > 0 && blockType !== "output" && jsPlumbInstance) {
      const outputCount = def.outputs.length;
      def.outputs.forEach((outLabel, i) => {
        const endpointUuid = `${blockId}-out-${i}`;
        const yPos = (i + 1) / (outputCount + 1);

        jsPlumbInstance.addEndpoint(blockId, {
          anchor: [1, yPos, 1, 0],
          uuid: endpointUuid
        }, {
          isSource: true,
          maxConnections: -1,
          overlays: [
            ["Label", {
              label: outLabel,
              location: [2.1, 1.5], /* Move label further right */
              cssClass: "endpointLabel rightLabel",
              labelStyle: {
                cssClass: 'endpointLabel',
                css: {
                  'text-align': 'left',
                  'white-space': 'nowrap'
                }
              }
            }]
          ]
        });
      });
    }

    // Delete block on click
    blockEl.querySelector('.delete-block').onclick = () => {
      // Remove endpoints and the block element
      if (jsPlumbInstance) {
        jsPlumbInstance.removeAllEndpoints(blockEl);
      }
      document.getElementById('canvas').removeChild(blockEl);
      // Remove from blocksData
      const idx = blocksData.findIndex(b => b.id === blockId);
      if (idx !== -1) blocksData.splice(idx, 1);
      // Remove any connections referencing this block
      for (let i = connections.length - 1; i >= 0; i--) {
        if (connections[i].sourceId === blockId || connections[i].targetId === blockId) {
          connections.splice(i, 1);
        }
      }
    };
  }

  /*****************************************************
   * Show/Hide Text Fields When Connected/Detached
   *****************************************************/
  function toggleTextFieldForConnection(targetEndpoint, connected) {
    const epUuid = targetEndpoint.getUuid();
    // We only hide the text field for input endpoints
    if (!epUuid.includes('-in-')) return;

    const [blockId, , inputIndex] = epUuid.split('-');
    const blockEl = document.getElementById(blockId);
    if (!blockEl) return;

    // Find the text field for that input index
    const inputField = blockEl.querySelector(`.inputField[data-input-index="${inputIndex}"]`);
    if (!inputField) return;

    // Hide if connected, show if disconnected
    if (connected) {
      inputField.classList.add('hidden');
    } else {
      inputField.classList.remove('hidden');
    }
  }

  /*****************************************************
   * Build Workflow Function
   * Returns the workflow object or null if validation fails
   *****************************************************/
  function buildWorkflow() {
    const logArea = document.getElementById('logArea');
    logArea.innerHTML = '';

    // Check if validation is disabled
    const disableValidation = document.getElementById('disableValidation').checked;

    // Validate blocks
    if (!disableValidation) {
      const validationError = validateBlocks();
      if (validationError) {
        showFeedback(validationError, "error");
        return null;
      }
    } else {
      showFeedback("Validation is disabled. Proceeding without validation.", "info");
    }

    // 1) Build adjacency for topological sort
    const n = blocksData.length;
    const adjacency = Array.from({length: n}, () => []);
    const inDegree = new Array(n).fill(0);

    connections.forEach(conn => {
      const sourceIndex = blocksData.findIndex(b => b.id === conn.sourceId);
      const targetIndex = blocksData.findIndex(b => b.id === conn.targetId);
      if (sourceIndex !== -1 && targetIndex !== -1) {
        adjacency[sourceIndex].push(targetIndex);
        inDegree[targetIndex]++;
      }
    });

    // 2) Kahn's Algorithm for topological sort
    const queue = [];
    for (let i = 0; i < n; i++) {
      if (inDegree[i] === 0) queue.push(i);
    }

    const topoOrder = [];
    while (queue.length > 0) {
      const cur = queue.shift();
      topoOrder.push(cur);
      adjacency[cur].forEach(neighbor => {
        inDegree[neighbor]--;
        if (inDegree[neighbor] === 0) {
          queue.push(neighbor);
        }
      });
    }

    // If there's a cycle and validation isn't disabled, error out
    if (topoOrder.length < n && !disableValidation) {
      showFeedback("Error: A cycle was detected. No valid topological order.", "error");
      return null;
    }

    // 3) Build steps in topological order
    const newIndex = new Array(n);
    topoOrder.forEach((oldIdx, sortedPos) => {
      newIndex[oldIdx] = sortedPos;
    });

    const finalSteps = topoOrder.map(oldIdx => {
      const blockObj = blocksData[oldIdx];
      const blockEl = document.getElementById(blockObj.id);
      const def = blockDefinitions[blockObj.type] || { 
        inputs: [], 
        inputTypes: [],
        settings: [], 
        settingsTypes: [],
        outputs: [], 
        outputTypes: []
      };

      // Gather typed inputs
      const inputValues = [];
      def.inputs.forEach((_, i) => {
        // If we have a text field (not hidden), collect it
        const inputField = blockEl.querySelector(`.inputField[data-input-index="${i}"]`);
        if (inputField && !inputField.classList.contains('hidden')) {
          const val = inputField.value.trim();
          if (val) {
            inputValues.push(val);
          }
        }
      });

      // If not "textInput", gather inbound references => "STEP$-$X"
      if (blockObj.type !== 'textInput') {
        def.inputs.forEach((_, i) => {
          const endpointUuid = `${blockObj.id}-in-${i}`;
          const c = connections.find(cnx =>
            cnx.targetId === blockObj.id && cnx.targetEndpointUuid === endpointUuid
          );
          if (c) {
            const sIndex = blocksData.findIndex(b => b.id === c.sourceId);
            if (sIndex !== -1) {
              const refPos = newIndex[sIndex];
              inputValues.push(`STEP$-$${refPos}`);
            }
          }
        });
      }

      // Gather settings (omit blank ones)
      const settingsMap = {};
      if (def.settings && def.settings.length > 0) {
        def.settings.forEach(settingName => {
          const settingField = blockEl.querySelector(
            `.settingField[data-setting-name="${settingName}"]`
          );
          if (settingField) {
            let sVal;
            // Determine the type to extract the value correctly
            const settingType = def.settingsTypes[def.settings.indexOf(settingName)].toLowerCase();
            if (settingType === "boolean") {
              sVal = settingField.checked;
            } else {
              sVal = settingField.value.trim();
            }

            if (sVal !== "") {
              settingsMap[settingName] = sVal;
            }
          }
        });
      }

      // Build final step object
      const stepData = {
        block: blockObj.type,
        inputs: inputValues
      };

      if (Object.keys(settingsMap).length > 0) {
        stepData.settings = settingsMap;
      }

      return stepData;
    });

    // 4) Construct final workflow object
    const workflow = {
      name: "Custom Workflow",
      steps: finalSteps
    };

    return workflow;
  }

  /*****************************************************
   * Validate blocks
   *****************************************************/
  function validateBlocks() {
    for (const blockObj of blocksData) {
      const blockEl = document.getElementById(blockObj.id);
      const def = blockDefinitions[blockObj.type];
      if (!def) continue;

      // Validate inputs
      if (def.inputs && def.inputs.length > 0) {
        for (let i = 0; i < def.inputs.length; i++) {
          const label = def.inputs[i];
          const endpointUuid = `${blockObj.id}-in-${i}`;

          // Check if there's a connection feeding into this input
          const connected = connections.some(conn =>
            conn.targetId === blockObj.id && conn.targetEndpointUuid === endpointUuid
          );

          // If not connected, must have a typed value
          if (!connected) {
            const inputField = blockEl.querySelector(`.inputField[data-input-index="${i}"]`);
            if (inputField && inputField.value.trim() === "") {
              return `Block "${def.displayName}" requires a value for "${label}" or a connection.`;
            }
          }
        }
      }

      // Validate settings
      if (def.settings && def.settings.length > 0) {
        for (let i = 0; i < def.settings.length; i++) {
          const settingName = def.settings[i];
          const settingField = blockEl.querySelector(
            `.settingField[data-setting-name="${settingName}"]`
          );
          if (settingField) {
            const settingType = def.settingsTypes[i].toLowerCase();
            if (settingType === "boolean") {
              // For boolean, no validation needed as checkbox is either checked or not
              continue;
            } else {
              if (settingField.value.trim() === "") {
                return `Block "${def.displayName}" requires a non-empty setting "${settingName}".`;
              }
            }
          }
        }
      }
    }
    return ""; // No errors found
  }

  /*****************************************************
   * Run Workflow
   *****************************************************/
  function runWorkflow() {
    showFeedback("");

    const workflow = buildWorkflow();
    if (!workflow) return; // If validation failed or cycle detected

    fetch('/api/submit-workflow', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(workflow)
    })
    .then(response => response.json())
    .then(data => {
      if (data.success) {
        showFeedback("Workflow ran on server!", "success");
      } else {
        showFeedback("Workflow submission failed: " + data.message, "error");
      }
      console.log('Server response:', data);
    })
    .catch(err => {
      showFeedback("Error sending workflow to server.", "error");
      console.error(err);
    });
  }

  /*****************************************************
   * Export JSON
   *****************************************************/
  function exportJSON() {
    showFeedback("");

    const workflow = buildWorkflow();
    if (!workflow) return;

    const jsonString = JSON.stringify(workflow, null, 2);
    const blob = new Blob([jsonString], { type: "application/json" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = 'workflow.json';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    showFeedback("Workflow exported as workflow.json", "success");
  }

  /*****************************************************
   * Pan and Zoom Functionality
   *****************************************************/
  (function() {
    const canvas = document.getElementById('canvas');
    let scale = 1;
    const scaleMultiplier = 0.1;
    const minScale = 0.1;
    const maxScale = 3;

    let isPanning = false;
    let startX = 0;
    let startY = 0;
    let translateX = 0;
    let translateY = 0;
    let lastTranslateX = 0;
    let lastTranslateY = 0;

    // Apply the current transformation
    function updateTransform() {
      canvas.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
      if (jsPlumbInstance) {
        jsPlumbInstance.repaintEverything(); // Repaint jsPlumb connections
      }
    }

    // Zoom In Function
    window.zoomIn = function() {
      if (scale < maxScale) {
        scale += scaleMultiplier;
        updateTransform();
      }
    };

    // Zoom Out Function
    window.zoomOut = function() {
      if (scale > minScale) {
        scale -= scaleMultiplier;
        updateTransform();
      }
    };

    // Reset Zoom Function
    window.resetZoom = function() {
      scale = 1;
      translateX = 0;
      translateY = 0;
      lastTranslateX = 0;
      lastTranslateY = 0;
      updateTransform();
    };

    // Handle Mouse Wheel for Zooming
    canvas.parentElement.addEventListener('wheel', function(e) {
      e.preventDefault();
      const delta = e.deltaY < 0 ? 1 : -1;
      const zoomFactor = 0.1;
      const newScale = scale + delta * zoomFactor;
      if (newScale >= minScale && newScale <= maxScale) {
        scale = newScale;
        updateTransform();
      }
    });

    // Handle Mouse Down for Panning
    canvas.parentElement.addEventListener('mousedown', function(e) {
      if (e.button !== 0) return; // Only respond to left-click
      isPanning = true;
      startX = e.clientX - lastTranslateX;
      startY = e.clientY - lastTranslateY;
      canvas.parentElement.classList.add('panning');
    });

    // Handle Mouse Move for Panning
    window.addEventListener('mousemove', function(e) {
      if (!isPanning) return;
      translateX = e.clientX - startX;
      translateY = e.clientY - startY;
      updateTransform();
    });

    // Handle Mouse Up to Stop Panning
    window.addEventListener('mouseup', function() {
      if (isPanning) {
        isPanning = false;
        lastTranslateX = translateX;
        lastTranslateY = translateY;
        canvas.parentElement.classList.remove('panning');
      }
    });

    // Handle Touch Events for Mobile Panning
    let touchStartX = 0;
    let touchStartY = 0;

    canvas.parentElement.addEventListener('touchstart', function(e) {
      if (e.touches.length === 1) {
        isPanning = true;
        touchStartX = e.touches[0].clientX - lastTranslateX;
        touchStartY = e.touches[0].clientY - lastTranslateY;
      }
    });

    canvas.parentElement.addEventListener('touchmove', function(e) {
      if (!isPanning || e.touches.length !== 1) return;
      translateX = e.touches[0].clientX - touchStartX;
      translateY = e.touches[0].clientY - touchStartY;
      updateTransform();
    });

    canvas.parentElement.addEventListener('touchend', function() {
      if (isPanning) {
        isPanning = false;
        lastTranslateX = translateX;
        lastTranslateY = translateY;
      }
    });

  })();

  /*****************************************************
   * Initialize Server-Sent Events (SSE) for logs
   *****************************************************/
  function initSSE() {
    let eventSource = null;
    const statusDot = document.getElementById('statusDot');
    const statusText = document.getElementById('statusText');
    let isFirstConnection = true; 

    let retryTimeout = 1000;
    const maxRetryTimeout = 30000;

    function connect() {
      eventSource = new EventSource('/events');

      eventSource.onopen = function() {
        console.log('SSE connection opened.');
        statusDot.classList.add('status-connected');
        statusText.textContent = 'Connected';
        
        if (!isFirstConnection) {
          showFeedback("Reconnected successfully!", "success");
        } else {
          isFirstConnection = false;
        }

        retryTimeout = 1000; // Reset on success
      };

      eventSource.onmessage = function(event) {
        if(event.data != "heartbeat") {
          const logArea = document.getElementById('logArea');
          
          // Check if the message starts with "Workflow result: "
          if (event.data.startsWith('Workflow result: ') && !event.data.includes('<html>')) {
            // Extract the text within the quotes
            const match = event.data.match(/Workflow result:\s*"([\s\S]*?)"/);
            if (match && match[1]) {
              const formattedText = match[1].replace(/\\n/g, '\n');
              const logMessage = document.createElement('div');
              logMessage.className = 'workflow-result';
              
              // To handle potential HTML in the text, use textContent
              logMessage.textContent = formattedText;

              logArea.appendChild(logMessage);
              logArea.scrollTop = logArea.scrollHeight;
            } else {
              // If regex doesn't match, treat it as a regular message
              const logMessage = document.createElement('div');
              logMessage.textContent = event.data;
              logArea.appendChild(logMessage);
              logArea.scrollTop = logArea.scrollHeight;
            }
          } else {
            // Regular log message
            const logMessage = document.createElement('div');
            logMessage.textContent = event.data;
            logArea.appendChild(logMessage);
            logArea.scrollTop = logArea.scrollHeight;
          }
        }
      };

      eventSource.onerror = function(err) {
        console.error('SSE error:', err);
        statusDot.classList.remove('status-connected');
        statusText.textContent = 'Disconnected';
        eventSource.close();

        setTimeout(() => {
          console.log('Attempting to reconnect SSE...');
          showFeedback("Connection lost. Attempting to reconnect...", "error");
          connect();
          retryTimeout = Math.min(retryTimeout * 2, maxRetryTimeout);
        }, retryTimeout);
      };
    }

    connect();
  }
</script>

</body>
</html>