Files
OpenNoodl/dev-docs/tasks/phase-3.5-realtime-agentic-ui/noodl-erleah-capability-analysis.md
2025-12-30 11:55:30 +01:00

22 KiB

Can Noodl Build the New Agentic Erleah?

Strategic Analysis & Roadmap

Date: December 30, 2025
Author: Strategic Analysis
Status: Recommendation - YES, with Phase 3.5 additions


Executive Summary

TL;DR: Yes, Noodl CAN build the new agentic Erleah, but it requires adding a focused "Phase 3.5: Real-Time Agentic UI" series of nodes and features. This is actually a PERFECT test case for Noodl's capabilities and would result in features that benefit the entire Noodl ecosystem.

Key Insights:

  1. Foundation is solid - Phases 1 & 2 created a modern React 19 + TypeScript base
  2. Core patterns exist - HTTP nodes, state management, and event systems are already there
  3. Missing pieces are specific - SSE streams, optimistic updates, and action dispatching
  4. High ROI - Building these features makes Noodl better for ALL modern web apps
  5. Validation opportunity - If Noodl can build Erleah, it proves the platform's maturity

Current Capabilities Assessment

What Noodl ALREADY Has

1. Modern Foundation (Phase 1 Complete)

  • React 19 in both editor and runtime
  • TypeScript 5 with full type inference
  • Modern tooling - webpack 5, Storybook 8
  • Performance - Build times improved, hot reload snappy

2. HTTP & API Integration (Phase 2 In Progress)

// Current HTTP Node capabilities:
-  GET/POST/PUT/DELETE/PATCH methods
-  Authentication presets (Bearer, Basic, API Key)
-  JSONPath response mapping
-  Header and query parameter management
-  Form data and URL-encoded bodies
-  Timeout configuration
-  Cancel requests

3. State Management

-  Variable nodes for local state
-  Object/Array manipulation nodes
-  Component Inputs/Outputs for prop drilling
-  Send Event/Receive Event for pub-sub
-  States node for state machines

4. Visual Components

-  Full React component library
-  Responsive breakpoints (planned in NODES-001)
-  Visual states (hover, pressed, disabled)
-  Conditional rendering
-  Repeater for dynamic lists

5. Event System

-  Signal-based event propagation
-  EventDispatcher for pub-sub patterns
-  Connection-based data flow
-  Debounce/Delay nodes for timing

What Noodl Is MISSING for Erleah

1. Server-Sent Events (SSE) Support

Current Gap: HTTP node only does request-response, no streaming

Erleah Needs:

// Chat messages streaming in real-time
AI Agent: "I'm searching attendees..." [streaming]
AI Agent: "Found 8 matches..." [streaming]
AI Agent: "Adding to your plan..." [streaming]

What's Required:

  • SSE connection node
  • Stream parsing (JSON chunks)
  • Progressive message accumulation
  • Automatic reconnection on disconnect

2. WebSocket Support

Current Gap: No WebSocket node exists

Erleah Needs:

// Real-time bidirectional communication
User  Backend: "Add this to timeline"
Backend  User: "Timeline updated" [instant]
Backend  User: "Connection request accepted" [push]

3. Optimistic UI Updates

Current Gap: No pattern for "update UI first, sync later"

Erleah Needs:

// Click "Accept" → immediate UI feedback
// Then backend call → roll back if it fails

What's Required:

  • Transaction/rollback state management
  • Pending state indicators
  • Error recovery patterns

4. Action Dispatcher Pattern

Current Gap: No concept of backend-triggered UI actions

Erleah Needs:

// Backend (AI Agent) sends:
{
  type: "OPEN_VIEW",
  view: "agenda",
  id: "session-123"
}

// Frontend automatically navigates

What's Required:

  • Action queue/processor
  • UI action vocabulary
  • Safe execution sandbox

5. State Synchronization Across Views

Current Gap: Component state is isolated, no global reactive store

Erleah Needs:

// Chat sidebar updates → Timeline view updates
// Timeline view updates → Parking Lot updates
// All views stay in sync automatically

What's Required:

  • Global observable store (like Zustand)
  • Subscription mechanism
  • Selective re-rendering

Gap Analysis: Erleah Requirements vs Noodl Capabilities

Feature Comparison Matrix

Erleah Feature Noodl Today Gap Size Effort to Add
Timeline View Repeater + Cards None 0 days
Chat Sidebar Components None 0 days
Parking Lot Sidebar Components None 0 days
Card Layouts Visual nodes None 0 days
HTTP API Calls HTTP Node None 0 days
Authentication Auth presets None 0 days
SSE Streaming None Large 3-5 days
WebSocket None Large 3-5 days
Optimistic Updates None Medium 2-3 days
Action Dispatcher ⚠️ Partial Medium 2-4 days
Global State ⚠️ Workarounds Small 2-3 days
State History None Small 1-2 days
Real-time Preview Existing None 0 days

Total New Development: ~15-24 days


Proposed Phase 3.5: Real-Time Agentic UI

Insert this between current Phase 3 and the rest of the roadmap.

Task Series: AGENT (AI Agent Integration)

Total Estimated: 15-24 days (3-4 weeks)

Task ID Name Estimate Description
AGENT-001 Server-Sent Events Node 3-5 days SSE connection, streaming, auto-reconnect
AGENT-002 WebSocket Node 3-5 days Bidirectional real-time communication
AGENT-003 Global State Store 2-3 days Observable store like Zustand, cross-component
AGENT-004 Optimistic Update Pattern 2-3 days Transaction wrapper, rollback support
AGENT-005 Action Dispatcher 2-4 days Backend-to-frontend command execution
AGENT-006 State History & Time Travel 1-2 days Undo/redo, state snapshots
AGENT-007 Stream Parser Utilities 2-3 days JSON streaming, chunk assembly

AGENT-001: Server-Sent Events Node

File: packages/noodl-runtime/src/nodes/std-library/data/ssenode.js

var SSENode = {
  name: 'net.noodl.SSE',
  displayNodeName: 'Server-Sent Events',
  docs: 'https://docs.noodl.net/nodes/data/sse',
  category: 'Data',
  color: 'data',
  searchTags: ['sse', 'stream', 'server-sent', 'events', 'realtime'],
  
  initialize: function() {
    this._internal.eventSource = null;
    this._internal.isConnected = false;
    this._internal.messageBuffer = [];
  },
  
  inputs: {
    url: {
      type: 'string',
      displayName: 'URL',
      group: 'Connection',
      set: function(value) {
        this._internal.url = value;
      }
    },
    
    connect: {
      type: 'signal',
      displayName: 'Connect',
      group: 'Actions',
      valueChangedToTrue: function() {
        this.doConnect();
      }
    },
    
    disconnect: {
      type: 'signal',
      displayName: 'Disconnect',
      group: 'Actions',
      valueChangedToTrue: function() {
        this.doDisconnect();
      }
    },
    
    autoReconnect: {
      type: 'boolean',
      displayName: 'Auto Reconnect',
      group: 'Connection',
      default: true
    },
    
    reconnectDelay: {
      type: 'number',
      displayName: 'Reconnect Delay (ms)',
      group: 'Connection',
      default: 3000
    }
  },
  
  outputs: {
    message: {
      type: 'object',
      displayName: 'Message',
      group: 'Data'
    },
    
    data: {
      type: '*',
      displayName: 'Parsed Data',
      group: 'Data'
    },
    
    connected: {
      type: 'signal',
      displayName: 'Connected',
      group: 'Events'
    },
    
    disconnected: {
      type: 'signal',
      displayName: 'Disconnected',
      group: 'Events'
    },
    
    error: {
      type: 'string',
      displayName: 'Error',
      group: 'Events'
    },
    
    isConnected: {
      type: 'boolean',
      displayName: 'Is Connected',
      group: 'Status',
      getter: function() {
        return this._internal.isConnected;
      }
    }
  },
  
  methods: {
    doConnect: function() {
      if (this._internal.eventSource) {
        this.doDisconnect();
      }
      
      const url = this._internal.url;
      if (!url) {
        this.setError('URL is required');
        return;
      }
      
      try {
        const eventSource = new EventSource(url);
        this._internal.eventSource = eventSource;
        
        eventSource.onopen = () => {
          this._internal.isConnected = true;
          this.flagOutputDirty('isConnected');
          this.sendSignalOnOutput('connected');
        };
        
        eventSource.onmessage = (event) => {
          this.handleMessage(event);
        };
        
        eventSource.onerror = (error) => {
          this._internal.isConnected = false;
          this.flagOutputDirty('isConnected');
          this.sendSignalOnOutput('disconnected');
          
          if (this._internal.autoReconnect) {
            setTimeout(() => {
              if (!this._internal.eventSource || this._internal.eventSource.readyState === EventSource.CLOSED) {
                this.doConnect();
              }
            }, this._internal.reconnectDelay || 3000);
          }
        };
        
      } catch (e) {
        this.setError(e.message);
      }
    },
    
    doDisconnect: function() {
      if (this._internal.eventSource) {
        this._internal.eventSource.close();
        this._internal.eventSource = null;
        this._internal.isConnected = false;
        this.flagOutputDirty('isConnected');
        this.sendSignalOnOutput('disconnected');
      }
    },
    
    handleMessage: function(event) {
      try {
        // Try to parse as JSON
        const data = JSON.parse(event.data);
        this._internal.message = event;
        this._internal.data = data;
      } catch (e) {
        // Not JSON, use raw data
        this._internal.message = event;
        this._internal.data = event.data;
      }
      
      this.flagOutputDirty('message');
      this.flagOutputDirty('data');
    },
    
    setError: function(message) {
      this._internal.error = message;
      this.flagOutputDirty('error');
    },
    
    _onNodeDeleted: function() {
      this.doDisconnect();
    }
  }
};

module.exports = {
  node: SSENode
};

AGENT-003: Global State Store Node

File: packages/noodl-runtime/src/nodes/std-library/data/globalstorenode.js

// Global store instance (singleton)
class GlobalStore {
  constructor() {
    this.stores = new Map();
    this.subscribers = new Map();
  }
  
  createStore(name, initialState = {}) {
    if (!this.stores.has(name)) {
      this.stores.set(name, initialState);
      this.subscribers.set(name, new Set());
    }
    return this.stores.get(name);
  }
  
  getState(name) {
    return this.stores.get(name) || {};
  }
  
  setState(name, updates) {
    const current = this.stores.get(name) || {};
    const next = { ...current, ...updates };
    this.stores.set(name, next);
    this.notify(name, next);
  }
  
  subscribe(name, callback) {
    if (!this.subscribers.has(name)) {
      this.subscribers.set(name, new Set());
    }
    this.subscribers.get(name).add(callback);
    
    // Return unsubscribe function
    return () => {
      this.subscribers.get(name).delete(callback);
    };
  }
  
  notify(name, state) {
    const subscribers = this.subscribers.get(name);
    if (subscribers) {
      subscribers.forEach(cb => cb(state));
    }
  }
}

const globalStoreInstance = new GlobalStore();

var GlobalStoreNode = {
  name: 'net.noodl.GlobalStore',
  displayNodeName: 'Global Store',
  category: 'Data',
  color: 'data',
  
  initialize: function() {
    this._internal.storeName = 'default';
    this._internal.unsubscribe = null;
  },
  
  inputs: {
    storeName: {
      type: 'string',
      displayName: 'Store Name',
      default: 'default',
      set: function(value) {
        if (this._internal.unsubscribe) {
          this._internal.unsubscribe();
        }
        this._internal.storeName = value;
        this.setupSubscription();
      }
    },
    
    set: {
      type: 'signal',
      displayName: 'Set',
      valueChangedToTrue: function() {
        this.doSet();
      }
    },
    
    key: {
      type: 'string',
      displayName: 'Key'
    },
    
    value: {
      type: '*',
      displayName: 'Value'
    }
  },
  
  outputs: {
    state: {
      type: 'object',
      displayName: 'State',
      getter: function() {
        return globalStoreInstance.getState(this._internal.storeName);
      }
    },
    
    stateChanged: {
      type: 'signal',
      displayName: 'State Changed'
    }
  },
  
  methods: {
    setupSubscription: function() {
      const storeName = this._internal.storeName;
      
      this._internal.unsubscribe = globalStoreInstance.subscribe(
        storeName,
        (newState) => {
          this.flagOutputDirty('state');
          this.sendSignalOnOutput('stateChanged');
        }
      );
      
      // Trigger initial state
      this.flagOutputDirty('state');
    },
    
    doSet: function() {
      const key = this._internal.key;
      const value = this._internal.value;
      const storeName = this._internal.storeName;
      
      if (key) {
        globalStoreInstance.setState(storeName, { [key]: value });
      }
    },
    
    _onNodeDeleted: function() {
      if (this._internal.unsubscribe) {
        this._internal.unsubscribe();
      }
    }
  }
};

AGENT-005: Action Dispatcher Node

File: packages/noodl-runtime/src/nodes/std-library/data/actiondispatchernode.js

var ActionDispatcherNode = {
  name: 'net.noodl.ActionDispatcher',
  displayNodeName: 'Action Dispatcher',
  category: 'Events',
  color: 'purple',
  
  initialize: function() {
    this._internal.actionHandlers = new Map();
    this._internal.pendingActions = [];
  },
  
  inputs: {
    action: {
      type: 'object',
      displayName: 'Action',
      set: function(value) {
        this._internal.currentAction = value;
        this.dispatch(value);
      }
    },
    
    // Register handlers
    registerHandler: {
      type: 'signal',
      displayName: 'Register Handler',
      valueChangedToTrue: function() {
        this.doRegisterHandler();
      }
    },
    
    handlerType: {
      type: 'string',
      displayName: 'Handler Type'
    },
    
    handlerCallback: {
      type: 'signal',
      displayName: 'Handler Callback'
    }
  },
  
  outputs: {
    actionType: {
      type: 'string',
      displayName: 'Action Type'
    },
    
    actionData: {
      type: 'object',
      displayName: 'Action Data'
    },
    
    dispatched: {
      type: 'signal',
      displayName: 'Dispatched'
    }
  },
  
  methods: {
    dispatch: function(action) {
      if (!action || !action.type) return;
      
      this._internal.actionType = action.type;
      this._internal.actionData = action.data || {};
      
      this.flagOutputDirty('actionType');
      this.flagOutputDirty('actionData');
      this.sendSignalOnOutput('dispatched');
      
      // Execute registered handlers
      const handler = this._internal.actionHandlers.get(action.type);
      if (handler) {
        handler(action.data);
      }
    },
    
    doRegisterHandler: function() {
      const type = this._internal.handlerType;
      if (!type) return;
      
      this._internal.actionHandlers.set(type, (data) => {
        this._internal.actionData = data;
        this.flagOutputDirty('actionData');
        this.sendSignalOnOutput('handlerCallback');
      });
    }
  }
};

Implementation Strategy: Phases 1-2-3.5-3-4-5

Revised Roadmap

Phase 1: Foundation ✅ COMPLETE
  ├─ React 19 migration
  ├─ TypeScript 5 upgrade
  └─ Storybook 8 migration

Phase 2: Core Features ⚙️ IN PROGRESS
  ├─ HTTP Node improvements ✅ COMPLETE
  ├─ Responsive breakpoints 🔄 ACTIVE
  ├─ Component migrations 🔄 ACTIVE
  └─ EventDispatcher React bridge ⚠️ BLOCKED

Phase 3.5: Real-Time Agentic UI 🆕 PROPOSED
  ├─ AGENT-001: SSE Node (3-5 days)
  ├─ AGENT-002: WebSocket Node (3-5 days)
  ├─ AGENT-003: Global State Store (2-3 days)
  ├─ AGENT-004: Optimistic Updates (2-3 days)
  ├─ AGENT-005: Action Dispatcher (2-4 days)
  ├─ AGENT-006: State History (1-2 days)
  └─ AGENT-007: Stream Utilities (2-3 days)
  
  Total: 15-24 days (3-4 weeks)

Phase 3: Advanced Features
  ├─ Dashboard UX (DASH series)
  ├─ Git Integration (GIT series)
  ├─ Shared Components (COMP series)
  ├─ AI Features (AI series)
  └─ Deployment (DEPLOY series)

Critical Path for Erleah

To build Erleah, this is the minimum required path:

Week 1-2: Phase 3.5 Core

  • AGENT-001 (SSE) - Absolutely critical for streaming AI responses
  • AGENT-003 (Global Store) - Required for synchronized state
  • AGENT-007 (Stream Utils) - Need to parse SSE JSON chunks

Week 3: Phase 3.5 Enhancement

  • AGENT-004 (Optimistic Updates) - Better UX for user interactions
  • AGENT-005 (Action Dispatcher) - AI agent can control UI

Week 4: Erleah Development

  • Build Timeline view
  • Build Chat sidebar with SSE
  • Build Parking Lot sidebar
  • Connect to backend

Total: 4 weeks to validated Erleah prototype in Noodl


Why This Is GOOD for Noodl

1. Validates Modern Architecture

Building a complex, agentic UI proves that Noodl's React 19 + TypeScript migration was worth it. This is a real-world stress test.

2. Features Benefit Everyone

SSE, WebSocket, and Global Store aren't "Erleah-specific" - every modern web app needs these:

  • Chat applications
  • Real-time dashboards
  • Collaborative tools
  • Live notifications
  • Streaming data visualization

3. Competitive Advantage

Flutterflow, Bubble, Webflow - none have agentic UI patterns built in. This would be a differentiator.

4. Dogfooding

Using Noodl to build a complex AI-powered app exposes UX issues and missing features that users face daily.

5. Marketing Asset

"Built with Noodl" becomes a powerful case study. Erleah is a sophisticated, modern web app that competes with pure-code solutions.


Risks & Mitigations

Risk 1: "We're adding too much complexity"

Mitigation: Phase 3.5 features are optional. Existing Noodl projects continue working. These are additive, not disruptive.

Risk 2: "What if we hit a fundamental limitation?"

Mitigation: Start with Phase 3.5 AGENT-001 (SSE) as a proof-of-concept. If that works smoothly, continue. If it's a nightmare, reconsider.

Risk 3: "We're delaying Phase 3 features"

Mitigation: Phase 3.5 is only 3-4 weeks. The learnings will inform Phase 3 (especially AI-001 AI Project Scaffolding).

Risk 4: "SSE/WebSocket are complex to implement correctly"

Mitigation: Leverage existing libraries (EventSource is native, WebSocket is native). Focus on the Noodl integration layer, not reinventing protocols.


Alternative: Hybrid Approach

If pure Noodl feels too risky, consider:

Option A: Noodl Editor + React Runtime

  • Build most of Erleah in Noodl
  • Write 1-2 custom React components for SSE streaming in pure code
  • Import as "Custom React Components" (already supported in Noodl)

Pros:

  • Faster initial development
  • No waiting for Phase 3.5
  • Still validates Noodl for 90% of the app

Cons:

  • Doesn't push Noodl forward
  • Misses opportunity to build reusable features

Option B: Erleah 1.0 in Code, Erleah 2.0 in Noodl

  • Ship current Erleah version in pure React
  • Use learnings to design Phase 3.5
  • Rebuild Erleah 2.0 in Noodl with Phase 3.5 features

Pros:

  • No business risk
  • Informs Phase 3.5 design with real requirements
  • Validates Noodl with second implementation

Cons:

  • Slower validation loop
  • Two separate codebases to maintain initially

Recommendation

Go Forward with Phase 3.5

Rationale:

  1. Timing is right - Phases 1 & 2 created the foundation
  2. Scope is focused - 7 tasks, 3-4 weeks, clear boundaries
  3. Value is high - Erleah validates Noodl, features benefit everyone
  4. Risk is manageable - Start with AGENT-001, can pivot if needed

📋 Action Plan

Immediate (Next 2 weeks):

  1. Create AGENT-001 (SSE Node) task document
  2. Implement SSE Node as proof-of-concept
  3. Build simple streaming chat UI in Noodl to test
  4. Evaluate: Did this feel natural? Were there blockers?

If POC succeeds (Week 3-4): 5. Complete AGENT-003 (Global Store) 6. Complete AGENT-007 (Stream Utils) 7. Build Erleah Timeline prototype in Noodl

If POC struggles: 8. Document specific pain points 9. Consider hybrid approach 10. Inform future node design

🎯 Success Criteria

Phase 3.5 is successful if:

  • SSE Node can stream AI responses smoothly
  • Global Store keeps views synchronized
  • Building Erleah in Noodl feels productive, not painful
  • The resulting app performs well (no visible lag)
  • Code is maintainable (not a tangled node spaghetti)

Conclusion

Can Noodl build the new agentic Erleah?

Yes - but only with Phase 3.5 additions. Without SSE, Global Store, and Action Dispatcher patterns, you'd be fighting the platform. With them, Noodl becomes a powerful tool for building modern, reactive, AI-powered web apps.

Should you do it?

Yes - this is a perfect validation moment. You've invested heavily in modernizing Noodl. Now prove it can build something cutting-edge. If Noodl struggles with Erleah, that's valuable feedback. If it succeeds, you have a compelling case study and a suite of new features.

Timeline:

  • Phase 3.5 Development: 3-4 weeks
  • Erleah Prototype: 1-2 weeks
  • Total to Validation: 4-6 weeks

This is a strategic investment that pays dividends beyond just Erleah.


Next Steps

  1. Review this document with the team
  2. Decide on approach: Full Phase 3.5, Hybrid, or Pure Code
  3. If Phase 3.5: Start with AGENT-001 task creation
  4. If Hybrid: Design the boundary between Noodl and custom React
  5. If Pure Code: Document learnings for future Noodl improvements

Question to answer: What would prove to you that Noodl CAN'T build Erleah? Define failure criteria upfront so you can pivot quickly if needed.