@webdevarif/reactflow
Version: 
Reusable ReactFlow components for social media bot automation
1 lines • 87.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/facebook/FacebookWorkflow.tsx","../src/facebook/components/MessageNodes.tsx","../src/facebook/config.ts","../src/components/ui/button.tsx","../src/lib/utils.ts","../src/components/ui/input.tsx","../src/components/ui/select.tsx","../src/components/ui/switch.tsx","../src/components/ui/textarea.tsx","../src/whatsapp/index.ts","../src/telegram/index.ts"],"sourcesContent":["import React, { useCallback, useState, useRef, useMemo } from 'react';\r\nimport ReactFlow, {\r\n  addEdge,\r\n  Background,\r\n  Controls,\r\n  MiniMap,\r\n  useNodesState,\r\n  useEdgesState,\r\n  Connection,\r\n  Edge,\r\n  Node,\r\n  ReactFlowProvider,\r\n  ReactFlowInstance,\r\n  NodeDragHandler,\r\n} from 'reactflow';\r\nimport 'reactflow/dist/style.css';\r\nimport { Plus, Settings, Trash2, Play, Save, Download, Upload } from 'lucide-react';\r\n\r\nimport { facebookNodeTypes, facebookEdgeTypes, facebookNodeTemplates, facebookSupportedMessageTypes } from './config';\r\nimport { WorkflowConfig } from '../types';\r\nimport { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea } from '../components/ui';\r\n\r\ninterface FacebookWorkflowProps {\r\n  initialNodes?: Node[];\r\n  initialEdges?: Edge[];\r\n  initialTitle?: string;\r\n  onSave?: (config: WorkflowConfig) => void;\r\n  onNodeClick?: (node: Node) => void;\r\n  onEdgeClick?: (edge: Edge) => void;\r\n  onTitleChange?: (title: string) => void;\r\n  className?: string;\r\n  height?: string | number;\r\n}\r\n\r\nconst FacebookWorkflow: React.FC<FacebookWorkflowProps> = ({\r\n  initialNodes = [],\r\n  initialEdges = [],\r\n  initialTitle = 'Facebook Bot Workflow',\r\n  onSave,\r\n  onNodeClick,\r\n  onEdgeClick,\r\n  onTitleChange,\r\n  className = '',\r\n  height = '600px'\r\n}) => {\r\n  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);\r\n  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);\r\n  const [selectedNode, setSelectedNode] = useState<Node | null>(null);\r\n  const [showTriggerPopup, setShowTriggerPopup] = useState(false);\r\n  const [showSidebar, setShowSidebar] = useState(true);\r\n  const [sidebarTab, setSidebarTab] = useState<'nodes' | 'properties'>('nodes');\r\n  const [isDragOver, setIsDragOver] = useState(false);\r\n  const [isClient, setIsClient] = useState(false);\r\n  const [forceUpdate, setForceUpdate] = useState(0);\r\n  const [workflowTitle, setWorkflowTitle] = useState(initialTitle);\r\n  const [isEditingTitle, setIsEditingTitle] = useState(false);\r\n  const reactFlowInstance = useRef<ReactFlowInstance | null>(null);\r\n\r\n  // Ensure component is client-side rendered to avoid hydration issues\r\n  React.useEffect(() => {\r\n    setIsClient(true);\r\n  }, []);\r\n\r\n  // Memoize nodeTypes and edgeTypes to prevent ReactFlow warnings\r\n  const memoizedNodeTypes = useMemo(() => facebookNodeTypes, []);\r\n  const memoizedEdgeTypes = useMemo(() => facebookEdgeTypes, []);\r\n\r\n  // Force re-render when selectedNode changes\r\n  const forceRerender = useCallback(() => {\r\n    setForceUpdate(prev => prev + 1);\r\n  }, []);\r\n\r\n  const handleTitleChange = useCallback((newTitle: string) => {\r\n    setWorkflowTitle(newTitle);\r\n    onTitleChange?.(newTitle);\r\n  }, [onTitleChange]);\r\n\r\n  const handleTitleSubmit = useCallback(() => {\r\n    setIsEditingTitle(false);\r\n    onTitleChange?.(workflowTitle);\r\n  }, [workflowTitle, onTitleChange]);\r\n\r\n  const handleTitleKeyDown = useCallback((e: React.KeyboardEvent) => {\r\n    if (e.key === 'Enter') {\r\n      handleTitleSubmit();\r\n    } else if (e.key === 'Escape') {\r\n      setWorkflowTitle(initialTitle);\r\n      setIsEditingTitle(false);\r\n    }\r\n  }, [handleTitleSubmit, initialTitle]);\r\n\r\n  const onConnect = useCallback(\r\n    (params: Connection) => setEdges((eds) => addEdge(params, eds)),\r\n    [setEdges]\r\n  );\r\n\r\n  const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {\r\n    setSelectedNode(node);\r\n    setSidebarTab('properties');\r\n    onNodeClick?.(node);\r\n  }, [onNodeClick]);\r\n\r\n  const handleEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {\r\n    onEdgeClick?.(edge);\r\n  }, [onEdgeClick]);\r\n\r\n  const addNode = useCallback((nodeType: string, position?: { x: number; y: number }) => {\r\n    const template = facebookNodeTemplates[nodeType as keyof typeof facebookNodeTemplates];\r\n    if (template) {\r\n      const newNode = {\r\n        ...template,\r\n        id: `${nodeType}_${Date.now()}`,\r\n        position: position || {\r\n          x: Math.random() * 400 + 200,\r\n          y: Math.random() * 400 + 200,\r\n        },\r\n      };\r\n      setNodes((nds) => [...nds, newNode]);\r\n    }\r\n    setShowTriggerPopup(false);\r\n  }, [setNodes]);\r\n\r\n  const onInit = useCallback((instance: ReactFlowInstance) => {\r\n    reactFlowInstance.current = instance;\r\n  }, []);\r\n\r\n  const onDragOver = useCallback((event: React.DragEvent) => {\r\n    event.preventDefault();\r\n    event.dataTransfer.dropEffect = 'move';\r\n    setIsDragOver(true);\r\n  }, []);\r\n\r\n  const onDrop = useCallback((event: React.DragEvent) => {\r\n    event.preventDefault();\r\n    setIsDragOver(false);\r\n    \r\n    const nodeType = event.dataTransfer.getData('application/reactflow');\r\n    if (!nodeType || !reactFlowInstance.current) {\r\n      return;\r\n    }\r\n\r\n    const position = reactFlowInstance.current.screenToFlowPosition({\r\n      x: event.clientX,\r\n      y: event.clientY,\r\n    });\r\n\r\n    addNode(nodeType, position);\r\n  }, [addNode]);\r\n\r\n  const onDragLeave = useCallback(() => {\r\n    setIsDragOver(false);\r\n  }, []);\r\n\r\n  const onDragStart = useCallback((event: React.DragEvent, nodeType: string) => {\r\n    event.dataTransfer.setData('application/reactflow', nodeType);\r\n    event.dataTransfer.effectAllowed = 'move';\r\n  }, []);\r\n\r\n  const deleteNode = useCallback((nodeId: string) => {\r\n    setNodes((nds) => nds.filter((node) => node.id !== nodeId));\r\n    setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));\r\n    setSelectedNode(null);\r\n  }, [setNodes, setEdges]);\r\n\r\n  const saveWorkflow = useCallback(() => {\r\n    const config: WorkflowConfig = {\r\n      name: workflowTitle,\r\n      description: 'Automated Facebook Messenger bot workflow',\r\n      platform: 'facebook',\r\n      nodes: nodes as any,\r\n      edges: edges as any,\r\n    };\r\n    onSave?.(config);\r\n  }, [nodes, edges, workflowTitle, onSave]);\r\n\r\n  const nodeGroups: Record<string, Array<{ type: string; label: string; icon: string; description: string }>> = {\r\n    'Triggers': [\r\n      { type: 'receive_message', label: 'Receive Message', icon: '📨', description: 'Triggered when user sends a message' },\r\n      { type: 'receive_comment', label: 'Receive Comment', icon: '💬', description: 'Triggered when user comments on post' },\r\n      { type: 'receive_post_reaction', label: 'Receive Post Reaction', icon: '👍', description: 'Triggered when user reacts to post' },\r\n      { type: 'receive_page_like', label: 'Receive Page Like', icon: '❤️', description: 'Triggered when user likes the page' },\r\n    ],\r\n    'Messages': [\r\n      { type: 'text', label: 'Text Message', icon: '💬', description: 'Send a text message' },\r\n      { type: 'image', label: 'Image Message', icon: '🖼️', description: 'Send an image' },\r\n      { type: 'audio', label: 'Audio Message', icon: '🎵', description: 'Send audio' },\r\n      { type: 'video', label: 'Video Message', icon: '🎥', description: 'Send video' },\r\n    ],\r\n    'Media': [\r\n      { type: 'file', label: 'File Message', icon: '📄', description: 'Send a file' },\r\n      { type: 'fb_media', label: 'Facebook Media', icon: '📱', description: 'Facebook media content' },\r\n      { type: 'carousel', label: 'Carousel', icon: '🎠', description: 'Carousel of items' },\r\n    ],\r\n    'Commerce': [\r\n      { type: 'ecommerce', label: 'E-commerce', icon: '🛒', description: 'E-commerce products' },\r\n    ],\r\n    'AI & Logic': [\r\n      { type: 'ai_reply', label: 'AI Reply', icon: '🤖', description: 'AI-powered response' },\r\n      { type: 'condition', label: 'Condition', icon: '🔀', description: 'Conditional logic' },\r\n      { type: 'delay', label: 'Delay', icon: '⏱️', description: 'Add delay' },\r\n    ],\r\n  };\r\n\r\n  // Show loading state until client-side hydration is complete\r\n  if (!isClient) {\r\n    return (\r\n      <div className={`w-full h-full bg-gray-50 flex items-center justify-center ${className}`} style={{ height }}>\r\n        <div className=\"text-center\">\r\n          <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4\"></div>\r\n          <p className=\"text-gray-600\">Loading workflow builder...</p>\r\n        </div>\r\n      </div>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <div className={`w-full h-full bg-gray-50 flex flex-col ${className}`} style={{ height }}>\r\n      {/* Top Toolbar */}\r\n      <div className=\"h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4\">\r\n        <div className=\"flex items-center gap-4\">\r\n          {isEditingTitle ? (\r\n            <Input\r\n              value={workflowTitle}\r\n              onChange={(e) => setWorkflowTitle(e.target.value)}\r\n              onBlur={handleTitleSubmit}\r\n              onKeyDown={handleTitleKeyDown}\r\n              className=\"text-lg font-semibold text-gray-900 bg-transparent border-none p-0 focus:ring-0 focus:border-none\"\r\n              autoFocus\r\n            />\r\n          ) : (\r\n            <h1 \r\n              className=\"text-lg font-semibold text-gray-900 cursor-pointer hover:bg-gray-100 px-2 py-1 rounded transition-colors\"\r\n              onClick={() => setIsEditingTitle(true)}\r\n              title=\"Click to edit workflow title\"\r\n            >\r\n              {workflowTitle}\r\n            </h1>\r\n          )}\r\n          <div className=\"flex items-center gap-2\">\r\n            <button\r\n              onClick={() => setShowSidebar(!showSidebar)}\r\n              className=\"p-2 hover:bg-gray-100 rounded-md transition-colors\"\r\n            >\r\n              <Settings className=\"w-4 h-4\" />\r\n            </button>\r\n          </div>\r\n        </div>\r\n        \r\n        <div className=\"flex items-center gap-2\">\r\n          <Button variant=\"outline\" size=\"sm\">\r\n            <Upload className=\"w-4 h-4 mr-2\" />\r\n            Import\r\n          </Button>\r\n          <Button variant=\"outline\" size=\"sm\">\r\n            <Download className=\"w-4 h-4 mr-2\" />\r\n            Export\r\n          </Button>\r\n          <Button variant=\"default\" size=\"sm\">\r\n            <Play className=\"w-4 h-4 mr-2\" />\r\n            Test\r\n          </Button>\r\n          <Button \r\n            onClick={saveWorkflow}\r\n            variant=\"default\"\r\n            size=\"sm\"\r\n            className=\"bg-green-600 hover:bg-green-700\"\r\n          >\r\n            <Save className=\"w-4 h-4 mr-2\" />\r\n            Save\r\n          </Button>\r\n        </div>\r\n      </div>\r\n\r\n      <div className=\"flex h-[calc(100%-3.5rem)] flex-1\">\r\n        {/* Sidebar */}\r\n        {showSidebar && (\r\n          <div className=\"w-80 bg-white border-r border-gray-200 flex flex-col h-full\">\r\n            {/* Sidebar Tabs */}\r\n            <div className=\"flex border-b border-gray-200\">\r\n              <button\r\n                onClick={() => setSidebarTab('nodes')}\r\n                className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${\r\n                  sidebarTab === 'nodes' \r\n                    ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50' \r\n                    : 'text-gray-600 hover:text-gray-900'\r\n                }`}\r\n              >\r\n                Nodes\r\n              </button>\r\n              <button\r\n                onClick={() => setSidebarTab('properties')}\r\n                className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${\r\n                  sidebarTab === 'properties' \r\n                    ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50' \r\n                    : 'text-gray-600 hover:text-gray-900'\r\n                }`}\r\n              >\r\n                Properties\r\n              </button>\r\n            </div>\r\n\r\n            {/* Sidebar Content */}\r\n            <div className=\"flex-1 overflow-y-auto\">\r\n              {sidebarTab === 'nodes' ? (\r\n                <div className=\"p-4\">\r\n                  {nodes.length === 0 ? (\r\n                    // Show only triggers when canvas is empty\r\n                    <div className=\"mb-6\">\r\n                      <h3 className=\"text-sm font-semibold text-gray-900 mb-3 uppercase tracking-wide\">\r\n                        Triggers\r\n                      </h3>\r\n                      <div className=\"space-y-2\">\r\n                        {nodeGroups['Triggers'].map((node) => (\r\n                          <div\r\n                            key={node.type}\r\n                            draggable\r\n                            onDragStart={(event) => onDragStart(event, node.type)}\r\n                            onClick={() => addNode(node.type)}\r\n                            className=\"w-full flex items-center gap-3 p-3 text-left hover:bg-gray-50 rounded-lg border border-gray-200 transition-colors group cursor-grab active:cursor-grabbing\"\r\n                          >\r\n                            <span className=\"text-2xl\">{node.icon}</span>\r\n                            <div className=\"flex-1 min-w-0\">\r\n                              <div className=\"text-sm font-medium text-gray-900 group-hover:text-blue-600\">\r\n                                {node.label}\r\n                              </div>\r\n                              <div className=\"text-xs text-gray-500 mt-0.5\">\r\n                                {node.description}\r\n                              </div>\r\n                            </div>\r\n                          </div>\r\n                        ))}\r\n                      </div>\r\n                    </div>\r\n                  ) : (\r\n                    // Show action nodes when canvas has nodes (hide triggers)\r\n                    Object.entries(nodeGroups)\r\n                      .filter(([groupName]) => groupName !== 'Triggers')\r\n                      .map(([groupName, groupNodes]) => (\r\n                        <div key={groupName} className=\"mb-6\">\r\n                          <h3 className=\"text-sm font-semibold text-gray-900 mb-3 uppercase tracking-wide\">\r\n                            {groupName}\r\n                          </h3>\r\n                          <div className=\"space-y-2\">\r\n                            {groupNodes.map((node) => (\r\n                              <div\r\n                                key={node.type}\r\n                                draggable\r\n                                onDragStart={(event) => onDragStart(event, node.type)}\r\n                                onClick={() => addNode(node.type)}\r\n                                className=\"w-full flex items-center gap-3 p-3 text-left hover:bg-gray-50 rounded-lg border border-gray-200 transition-colors group cursor-grab active:cursor-grabbing\"\r\n                              >\r\n                                <span className=\"text-2xl\">{node.icon}</span>\r\n                                <div className=\"flex-1 min-w-0\">\r\n                                  <div className=\"text-sm font-medium text-gray-900 group-hover:text-blue-600\">\r\n                                    {node.label}\r\n                                  </div>\r\n                                  <div className=\"text-xs text-gray-500 mt-0.5\">\r\n                                    {node.description}\r\n                                  </div>\r\n                                </div>\r\n                              </div>\r\n                            ))}\r\n                          </div>\r\n                        </div>\r\n                      ))\r\n                  )}\r\n                </div>\r\n              ) : (\r\n                <div className=\"p-4\">\r\n                  {selectedNode ? (\r\n                    <div className=\"space-y-4\">\r\n                      <div>\r\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                          Node Type\r\n                        </label>\r\n                        <div className=\"text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md\">\r\n                          {selectedNode.type}\r\n                        </div>\r\n                      </div>\r\n                      <div>\r\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                          Label\r\n                        </label>\r\n                        <Input\r\n                          type=\"text\"\r\n                          value={selectedNode.data.label}\r\n                          onChange={(e) => {\r\n                            setNodes((nds) => nds.map((n) => \r\n                              n.id === selectedNode.id \r\n                                ? { ...n, data: { ...n.data, label: e.target.value } }\r\n                                : n\r\n                            ));\r\n                            // Update selectedNode to reflect changes\r\n                            setSelectedNode(prev => prev ? { ...prev, data: { ...prev.data, label: e.target.value } } : null);\r\n                            forceRerender();\r\n                          }}\r\n                        />\r\n                      </div>\r\n                      {/* Show content field only for non-trigger nodes */}\r\n                      {selectedNode.data.type !== 'trigger' && selectedNode.data.content !== undefined && (\r\n                        <div>\r\n                          <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                            Content\r\n                          </label>\r\n                          <Textarea\r\n                            value={selectedNode.data.content || ''}\r\n                            onChange={(e) => {\r\n                              setNodes((nds) => nds.map((n) => \r\n                                n.id === selectedNode.id \r\n                                  ? { ...n, data: { ...n.data, content: e.target.value } }\r\n                                  : n\r\n                              ));\r\n                              setSelectedNode(prev => prev ? { ...prev, data: { ...prev.data, content: e.target.value } } : null);\r\n                              forceRerender();\r\n                            }}\r\n                            rows={3}\r\n                          />\r\n                        </div>\r\n                      )}\r\n                      \r\n                      {/* Trigger-specific properties */}\r\n                      {selectedNode.data.type === 'trigger' && (\r\n                        <>\r\n                          <div>\r\n                            <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                              Keywords\r\n                            </label>\r\n                            <Textarea\r\n                              placeholder=\"hello, help, {{ body }}\"\r\n                              value={selectedNode.data.metadata?.keywords || ''}\r\n                              onChange={(e) => {\r\n                                setNodes((nds) => nds.map((n) => \r\n                                  n.id === selectedNode.id \r\n                                    ? { \r\n                                        ...n, \r\n                                        data: { \r\n                                          ...n.data, \r\n                                          metadata: { \r\n                                            ...n.data.metadata, \r\n                                            keywords: e.target.value \r\n                                          } \r\n                                        } \r\n                                      }\r\n                                    : n\r\n                                ));\r\n                                // Update selectedNode to reflect changes\r\n                                setSelectedNode(prev => prev ? { \r\n                                  ...prev, \r\n                                  data: { \r\n                                    ...prev.data, \r\n                                    metadata: { \r\n                                      ...prev.data.metadata, \r\n                                      keywords: e.target.value \r\n                                    } \r\n                                  } \r\n                                } : null);\r\n                                forceRerender();\r\n                              }}\r\n                              rows={3}\r\n                            />\r\n                            <p className=\"text-xs text-gray-500 mt-1\">\r\n                              Comma separated keywords or {`{{ body }}`} for full content\r\n                            </p>\r\n                          </div>\r\n                          \r\n                          <div>\r\n                            <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                              Match Type\r\n                            </label>\r\n                            <Select\r\n                              value={selectedNode.data.metadata?.matchType || 'contains'}\r\n                              onValueChange={(value) => {\r\n                                setNodes((nds) => nds.map((n) => \r\n                                  n.id === selectedNode.id \r\n                                    ? { \r\n                                        ...n, \r\n                                        data: { \r\n                                          ...n.data, \r\n                                          metadata: { \r\n                                            ...n.data.metadata, \r\n                                            matchType: value \r\n                                          } \r\n                                        } \r\n                                      }\r\n                                    : n\r\n                                ));\r\n                                // Update selectedNode to reflect changes\r\n                                setSelectedNode(prev => prev ? { \r\n                                  ...prev, \r\n                                  data: { \r\n                                    ...prev.data, \r\n                                    metadata: { \r\n                                      ...prev.data.metadata, \r\n                                      matchType: value \r\n                                    } \r\n                                  } \r\n                                } : null);\r\n                                forceRerender();\r\n                              }}\r\n                            >\r\n                              <SelectTrigger>\r\n                                <SelectValue placeholder=\"Select match type\" />\r\n                              </SelectTrigger>\r\n                              <SelectContent>\r\n                                <SelectItem value=\"contains\">Contains</SelectItem>\r\n                                <SelectItem value=\"exact\">Exact Match</SelectItem>\r\n                                <SelectItem value=\"starts_with\">Starts With</SelectItem>\r\n                                <SelectItem value=\"ends_with\">Ends With</SelectItem>\r\n                              </SelectContent>\r\n                            </Select>\r\n                          </div>\r\n                          \r\n                          <div className=\"flex items-center justify-between\">\r\n                            <div>\r\n                              <label htmlFor=\"exactMatch\" className=\"text-sm font-medium text-gray-700\">\r\n                                Exact Match (case sensitive)\r\n                              </label>\r\n                              <p className=\"text-xs text-gray-500 mt-1\">\r\n                                Enable case-sensitive matching\r\n                              </p>\r\n                            </div>\r\n                            <div className=\"flex items-center\">\r\n                              <Switch\r\n                                checked={selectedNode.data.metadata?.exactMatch || false}\r\n                                onCheckedChange={(checked) => {\r\n                                  console.log('Switch toggled:', { checked, nodeId: selectedNode.id });\r\n                                  \r\n                                  // Update nodes state\r\n                                  setNodes((nds) => nds.map((n) => \r\n                                    n.id === selectedNode.id \r\n                                      ? { \r\n                                          ...n, \r\n                                          data: { \r\n                                            ...n.data, \r\n                                            metadata: { \r\n                                              ...n.data.metadata, \r\n                                              exactMatch: checked \r\n                                            } \r\n                                          } \r\n                                        }\r\n                                      : n\r\n                                  ));\r\n                                  \r\n                                  // Update selectedNode state\r\n                                  setSelectedNode(prev => prev ? { \r\n                                    ...prev, \r\n                                    data: { \r\n                                      ...prev.data, \r\n                                      metadata: { \r\n                                        ...prev.data.metadata, \r\n                                        exactMatch: checked \r\n                                      } \r\n                                    } \r\n                                  } : null);\r\n                                }}\r\n                              />\r\n                            </div>\r\n                          </div>\r\n                        </>\r\n                      )}\r\n                      \r\n                      {/* Delay-specific properties */}\r\n                      {selectedNode.data.type === 'delay' && (\r\n                        <div>\r\n                          <label className=\"block text-sm font-medium text-gray-700 mb-2\">\r\n                            Delay Duration (milliseconds)\r\n                          </label>\r\n                          <Input\r\n                            type=\"number\"\r\n                            min=\"0\"\r\n                            step=\"100\"\r\n                            value={selectedNode.data.metadata?.delay || 300}\r\n                            onChange={(e) => {\r\n                              const delayValue = parseInt(e.target.value) || 300;\r\n                              setNodes((nds) => nds.map((n) => \r\n                                n.id === selectedNode.id \r\n                                  ? { \r\n                                      ...n, \r\n                                      data: { \r\n                                        ...n.data, \r\n                                        metadata: { \r\n                                          ...n.data.metadata, \r\n                                          delay: delayValue \r\n                                        } \r\n                                      } \r\n                                    }\r\n                                  : n\r\n                              ));\r\n                              // Update selectedNode to reflect changes\r\n                              setSelectedNode(prev => prev ? { \r\n                                ...prev, \r\n                                data: { \r\n                                  ...prev.data, \r\n                                  metadata: { \r\n                                    ...prev.data.metadata, \r\n                                    delay: delayValue \r\n                                  } \r\n                                } \r\n                              } : null);\r\n                              forceRerender();\r\n                            }}\r\n                            placeholder=\"300\"\r\n                          />\r\n                          <p className=\"text-xs text-gray-500 mt-1\">\r\n                            Delay in milliseconds (default: 300ms)\r\n                          </p>\r\n                        </div>\r\n                      )}\r\n                      \r\n                      <Button\r\n                        onClick={() => deleteNode(selectedNode.id)}\r\n                        variant=\"destructive\"\r\n                        className=\"w-full\"\r\n                      >\r\n                        <Trash2 className=\"w-4 h-4 mr-2\" />\r\n                        Delete Node\r\n                      </Button>\r\n                    </div>\r\n                  ) : (\r\n                    <div className=\"text-center py-8\">\r\n                      <div className=\"text-gray-400 mb-2\">\r\n                        <Settings className=\"w-8 h-8 mx-auto\" />\r\n                      </div>\r\n                      <p className=\"text-sm text-gray-500\">\r\n                        Select a node to edit its properties\r\n                      </p>\r\n                    </div>\r\n                  )}\r\n                </div>\r\n              )}\r\n            </div>\r\n          </div>\r\n        )}\r\n\r\n        {/* Main Canvas */}\r\n        <div className={`flex-1 relative ${isDragOver ? 'bg-blue-50 border-2 border-dashed border-blue-300' : ''}`}>\r\n          <ReactFlow\r\n            nodes={nodes}\r\n            edges={edges}\r\n            onNodesChange={onNodesChange}\r\n            onEdgesChange={onEdgesChange}\r\n            onConnect={onConnect}\r\n            onNodeClick={handleNodeClick}\r\n            onEdgeClick={handleEdgeClick}\r\n            onInit={onInit}\r\n            onDrop={onDrop}\r\n            onDragOver={onDragOver}\r\n            onDragLeave={onDragLeave}\r\n              nodeTypes={memoizedNodeTypes}\r\n            fitView\r\n            attributionPosition=\"bottom-left\"\r\n            className=\"bg-gray-50\"\r\n          >\r\n            <Background color=\"#e5e7eb\" gap={20} />\r\n            <Controls className=\"bg-white border border-gray-200 rounded-lg shadow-sm\" />\r\n            <MiniMap \r\n              className=\"bg-white border border-gray-200 rounded-lg shadow-sm\"\r\n              nodeColor=\"#3b82f6\"\r\n              maskColor=\"rgba(0, 0, 0, 0.1)\"\r\n            />\r\n          </ReactFlow>\r\n\r\n          {/* Center Plus Button (n8n style) */}\r\n          {nodes.length === 0 && !isDragOver && (\r\n            <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none\">\r\n              <div className=\"text-center\">\r\n                <button\r\n                  onClick={() => setShowTriggerPopup(true)}\r\n                  className=\"mx-auto w-16 h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center pointer-events-auto group\"\r\n                >\r\n                  <Plus className=\"w-8 h-8 group-hover:scale-110 transition-transform\" />\r\n                </button>\r\n                <p className=\"mt-4 text-sm text-gray-500 font-medium\">\r\n                  Click to add your first node\r\n                </p>\r\n              </div>\r\n            </div>\r\n          )}\r\n\r\n          {/* Drop Zone Indicator */}\r\n          {isDragOver && (\r\n            <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none\">\r\n              <div className=\"text-center\">\r\n                <div className=\"w-20 h-20 bg-blue-100 border-4 border-dashed border-blue-400 rounded-full flex items-center justify-center\">\r\n                  <Plus className=\"w-10 h-10 text-blue-600\" />\r\n                </div>\r\n                <p className=\"mt-4 text-lg text-blue-600 font-semibold\">\r\n                  Drop node here\r\n                </p>\r\n              </div>\r\n            </div>\r\n          )}\r\n        </div>\r\n      </div>\r\n\r\n      {/* Trigger Popup */}\r\n      {showTriggerPopup && (\r\n        <div className=\"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50\">\r\n          <div className=\"bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden\">\r\n            <div className=\"p-6 border-b border-gray-200\">\r\n              <div className=\"flex items-center justify-between\">\r\n                <h2 className=\"text-lg font-semibold text-gray-900\">Choose a Trigger</h2>\r\n                <button\r\n                  onClick={() => setShowTriggerPopup(false)}\r\n                  className=\"text-gray-400 hover:text-gray-600\"\r\n                >\r\n                  <Plus className=\"w-6 h-6 rotate-45\" />\r\n                </button>\r\n              </div>\r\n            </div>\r\n            <div className=\"p-6 overflow-y-auto max-h-[60vh]\">\r\n              {/* Show only triggers in popup */}\r\n              <div className=\"mb-6\">\r\n                <h3 className=\"text-sm font-semibold text-gray-900 mb-3 uppercase tracking-wide\">\r\n                  Triggers\r\n                </h3>\r\n                <div className=\"grid grid-cols-2 gap-3\">\r\n                  {nodeGroups['Triggers'].map((node) => (\r\n                    <div\r\n                      key={node.type}\r\n                      draggable\r\n                      onDragStart={(event) => onDragStart(event, node.type)}\r\n                      onClick={() => addNode(node.type)}\r\n                      className=\"flex items-center gap-3 p-4 text-left hover:bg-gray-50 rounded-lg border border-gray-200 transition-colors group cursor-grab active:cursor-grabbing\"\r\n                    >\r\n                      <span className=\"text-2xl\">{node.icon}</span>\r\n                      <div className=\"flex-1 min-w-0\">\r\n                        <div className=\"text-sm font-medium text-gray-900 group-hover:text-blue-600\">\r\n                          {node.label}\r\n                        </div>\r\n                        <div className=\"text-xs text-gray-500 mt-0.5\">\r\n                          {node.description}\r\n                        </div>\r\n                      </div>\r\n                    </div>\r\n                  ))}\r\n                </div>\r\n              </div>\r\n            </div>\r\n          </div>\r\n        </div>\r\n      )}\r\n    </div>\r\n  );\r\n};\r\n\r\n// Wrapper with ReactFlowProvider\r\nconst FacebookWorkflowWithProvider: React.FC<FacebookWorkflowProps> = (props) => (\r\n  <ReactFlowProvider>\r\n    <FacebookWorkflow {...props} />\r\n  </ReactFlowProvider>\r\n);\r\n\r\nexport default FacebookWorkflowWithProvider;\r\nexport { FacebookWorkflow };\r\n","import React from 'react';\r\nimport { Handle, Position } from 'reactflow';\r\nimport { \r\n  MessageSquare, \r\n  Image, \r\n  Music, \r\n  Video, \r\n  FileText, \r\n  ShoppingCart, \r\n  Bot,\r\n  Car,\r\n  Sparkles\r\n} from 'lucide-react';\r\nimport { BaseNodeProps } from '../../types';\r\n\r\n// Base Node Component\r\nconst BaseNode: React.FC<BaseNodeProps> = ({ data, selected }) => {\r\n  const getIcon = (type: string) => {\r\n    switch (type) {\r\n      case 'text': return '💬';\r\n      case 'image': return '🖼️';\r\n      case 'audio': return '🎵';\r\n      case 'video': return '🎥';\r\n      case 'file': return '📄';\r\n      case 'fb_media': return '📱';\r\n      case 'carousel': return '🎠';\r\n      case 'ecommerce': return '🛒';\r\n      case 'ai_reply': return '🤖';\r\n      default: return '⚙️';\r\n    }\r\n  };\r\n\r\n  const getColor = (type: string) => {\r\n    switch (type) {\r\n      case 'text': return 'bg-blue-50 border-blue-200 text-blue-800';\r\n      case 'image': return 'bg-green-50 border-green-200 text-green-800';\r\n      case 'audio': return 'bg-purple-50 border-purple-200 text-purple-800';\r\n      case 'video': return 'bg-red-50 border-red-200 text-red-800';\r\n      case 'file': return 'bg-gray-50 border-gray-200 text-gray-800';\r\n      case 'fb_media': return 'bg-indigo-50 border-indigo-200 text-indigo-800';\r\n      case 'carousel': return 'bg-yellow-50 border-yellow-200 text-yellow-800';\r\n      case 'ecommerce': return 'bg-pink-50 border-pink-200 text-pink-800';\r\n      case 'ai_reply': return 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 text-purple-800';\r\n      default: return 'bg-gray-50 border-gray-200 text-gray-800';\r\n    }\r\n  };\r\n\r\n  return (\r\n    <div className={`px-4 py-3 shadow-sm rounded-lg border-2 min-w-[200px] max-w-[250px] ${getColor(data.type)} ${selected ? 'ring-2 ring-blue-500 shadow-lg' : 'hover:shadow-md'} transition-all duration-200`}>\r\n      <div className=\"flex items-center gap-3\">\r\n        <span className=\"text-xl\">{getIcon(data.type)}</span>\r\n        <div className=\"flex-1 min-w-0\">\r\n          <div className=\"font-semibold text-sm truncate\">{data.label}</div>\r\n          {data.content && (\r\n            <div className=\"text-xs text-gray-600 mt-1 line-clamp-2\">\r\n              {data.content}\r\n            </div>\r\n          )}\r\n        </div>\r\n      </div>\r\n      \r\n      {data.mediaUrl && (\r\n        <div className=\"mt-2 text-xs text-gray-500 flex items-center gap-1\">\r\n          <span>📎</span>\r\n          <span>{data.mediaType || 'media'}</span>\r\n        </div>\r\n      )}\r\n\r\n      <Handle\r\n        type=\"target\"\r\n        position={Position.Top}\r\n        className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-blue-500\"\r\n        style={{ top: -6 }}\r\n      />\r\n      <Handle\r\n        type=\"source\"\r\n        position={Position.Bottom}\r\n        className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-blue-500\"\r\n        style={{ bottom: -6 }}\r\n      />\r\n    </div>\r\n  );\r\n};\r\n\r\n// Text Message Node\r\nexport const TextMessageNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Image Message Node\r\nexport const ImageMessageNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Audio Message Node\r\nexport const AudioMessageNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Video Message Node\r\nexport const VideoMessageNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// File Message Node\r\nexport const FileMessageNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Facebook Media Node\r\nexport const FacebookMediaNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Carousel Node\r\nexport const CarouselNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Ecommerce Node\r\nexport const EcommerceNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// AI Reply Node\r\nexport const AIReplyNode: React.FC<BaseNodeProps> = (props) => (\r\n  <BaseNode {...props} />\r\n);\r\n\r\n// Condition Node\r\nexport const ConditionNode: React.FC<BaseNodeProps> = ({ data, selected }) => (\r\n  <div className={`px-4 py-3 shadow-sm rounded-lg border-2 min-w-[200px] max-w-[250px] bg-orange-50 border-orange-200 text-orange-800 ${selected ? 'ring-2 ring-orange-500 shadow-lg' : 'hover:shadow-md'} transition-all duration-200`}>\r\n    <div className=\"flex items-center gap-3\">\r\n      <span className=\"text-xl\">🔀</span>\r\n      <div className=\"flex-1 min-w-0\">\r\n        <div className=\"font-semibold text-sm truncate\">{data.label}</div>\r\n        {data.metadata?.condition && (\r\n          <div className=\"text-xs text-gray-600 mt-1\">\r\n            {data.metadata.condition}\r\n          </div>\r\n        )}\r\n      </div>\r\n    </div>\r\n\r\n    <Handle\r\n      type=\"target\"\r\n      position={Position.Top}\r\n      className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-orange-500\"\r\n      style={{ top: -6 }}\r\n    />\r\n    <Handle\r\n      type=\"source\"\r\n      position={Position.Bottom}\r\n      className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-orange-500\"\r\n      style={{ bottom: -6 }}\r\n    />\r\n  </div>\r\n);\r\n\r\n// Delay Node\r\nexport const DelayNode: React.FC<BaseNodeProps> = ({ data, selected }) => (\r\n  <div className={`px-4 py-3 shadow-sm rounded-lg border-2 min-w-[200px] max-w-[250px] bg-cyan-50 border-cyan-200 text-cyan-800 ${selected ? 'ring-2 ring-cyan-500 shadow-lg' : 'hover:shadow-md'} transition-all duration-200`}>\r\n    <div className=\"flex items-center gap-3\">\r\n      <span className=\"text-xl\">⏱️</span>\r\n      <div className=\"flex-1 min-w-0\">\r\n        <div className=\"font-semibold text-sm truncate\">{data.label}</div>\r\n        {data.metadata?.delay && (\r\n          <div className=\"text-xs text-gray-600 mt-1\">\r\n            {(data.metadata.delay / 1000).toFixed(1)}s\r\n          </div>\r\n        )}\r\n      </div>\r\n    </div>\r\n\r\n    <Handle\r\n      type=\"target\"\r\n      position={Position.Top}\r\n      className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-cyan-500\"\r\n      style={{ top: -6 }}\r\n    />\r\n    <Handle\r\n      type=\"source\"\r\n      position={Position.Bottom}\r\n      className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-cyan-500\"\r\n      style={{ bottom: -6 }}\r\n    />\r\n  </div>\r\n);\r\n\r\n// Trigger Node (Base for all triggers)\r\nexport const TriggerNode: React.FC<BaseNodeProps> = ({ data, selected }) => {\r\n  const getIcon = (type: string) => {\r\n    switch (type) {\r\n      case 'receive_message': return '💬'; // Facebook Messenger icon\r\n      case 'receive_comment': return '💬';\r\n      case 'receive_post_reaction': return '👍';\r\n      case 'receive_page_like': return '❤️';\r\n      default: return '⚡';\r\n    }\r\n  };\r\n\r\n  return (\r\n    <div className={`px-4 py-3 shadow-sm rounded-lg border-2 min-w-[200px] max-w-[250px] bg-green-50 border-green-200 text-green-800 ${selected ? 'ring-2 ring-green-500 shadow-lg' : 'hover:shadow-md'} transition-all duration-200`}>\r\n      <div className=\"flex items-center gap-3\">\r\n        <span className=\"text-xl\">{getIcon(data.type)}</span>\r\n        <div className=\"flex-1 min-w-0\">\r\n          <div className=\"font-semibold text-sm truncate\">{data.label}</div>\r\n          {data.metadata?.keywords && (\r\n            <div className=\"text-xs text-green-600 mt-1 font-medium\">\r\n              Keywords: {data.metadata.keywords}\r\n            </div>\r\n          )}\r\n        </div>\r\n      </div>\r\n      \r\n      <div className=\"mt-2 text-xs text-green-600 font-medium\">\r\n        TRIGGER\r\n      </div>\r\n\r\n      <Handle\r\n        type=\"source\"\r\n        position={Position.Bottom}\r\n        className=\"w-3 h-3 bg-white border-2 border-gray-400 hover:border-green-500\"\r\n        style={{ bottom: -6 }}\r\n      />\r\n    </div>\r\n  );\r\n};\r\n","import {\r\n  TextMessageNode,\r\n  ImageMessageNode,\r\n  AudioMessageNode,\r\n  VideoMessageNode,\r\n  FileMessageNode,\r\n  FacebookMediaNode,\r\n  CarouselNode,\r\n  EcommerceNode,\r\n  AIReplyNode,\r\n  ConditionNode,\r\n  DelayNode,\r\n  TriggerNode\r\n} from './components/MessageNodes';\r\n\r\nexport const facebookNodeTypes = {\r\n  // Trigger nodes\r\n  receive_message: TriggerNode,\r\n  receive_comment: TriggerNode,\r\n  receive_post_reaction: TriggerNode,\r\n  receive_page_like: TriggerNode,\r\n  // Action nodes\r\n  text: TextMessageNode,\r\n  image: ImageMessageNode,\r\n  audio: AudioMessageNode,\r\n  video: VideoMessageNode,\r\n  file: FileMessageNode,\r\n  fb_media: FacebookMediaNode,\r\n  carousel: CarouselNode,\r\n  ecommerce: EcommerceNode,\r\n  ai_reply: AIReplyNode,\r\n  condition: ConditionNode,\r\n  delay: DelayNode,\r\n};\r\n\r\nexport const facebookEdgeTypes = {\r\n  default: 'smoothstep',\r\n  conditional: 'smoothstep',\r\n};\r\n\r\nexport const facebookSupportedMessageTypes = [\r\n  'text',\r\n  'image', \r\n  'audio',\r\n  'video',\r\n  'file',\r\n  'fb_media',\r\n  'carousel',\r\n  'ecommerce',\r\n  'ai_reply'\r\n];\r\n\r\nexport const facebookNodeTemplates = {\r\n  // Trigger nodes\r\n  receive_message: {\r\n    type: 'receive_message',\r\n    data: {\r\n      label: 'Receive Message',\r\n      type: 'trigger',\r\n      metadata: {\r\n        triggerType: 'message',\r\n        event: 'message_received',\r\n        keywords: '{{ body }}',\r\n        matchType: 'contains',\r\n        exactMatch: false\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  receive_comment: {\r\n    type: 'receive_comment',\r\n    data: {\r\n      label: 'Receive Comment',\r\n      type: 'trigger',\r\n      metadata: {\r\n        triggerType: 'comment',\r\n        event: 'comment_received',\r\n        keywords: '{{ body }}',\r\n        matchType: 'contains',\r\n        exactMatch: false\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  receive_post_reaction: {\r\n    type: 'receive_post_reaction',\r\n    data: {\r\n      label: 'Receive Post Reaction',\r\n      type: 'trigger',\r\n      metadata: {\r\n        triggerType: 'reaction',\r\n        event: 'post_reaction'\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  receive_page_like: {\r\n    type: 'receive_page_like',\r\n    data: {\r\n      label: 'Receive Page Like',\r\n      type: 'trigger',\r\n      metadata: {\r\n        triggerType: 'page_like',\r\n        event: 'page_liked'\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  // Action nodes\r\n  text: {\r\n    type: 'text',\r\n    data: {\r\n      label: 'Text Message',\r\n      type: 'text',\r\n      content: 'Hello! How can I help you today?',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  image: {\r\n    type: 'image',\r\n    data: {\r\n      label: 'Image Message',\r\n      type: 'image',\r\n      content: 'Check out this image!',\r\n      mediaType: 'image',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  audio: {\r\n    type: 'audio',\r\n    data: {\r\n      label: 'Audio Message',\r\n      type: 'audio',\r\n      content: 'Listen to this audio',\r\n      mediaType: 'audio',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  video: {\r\n    type: 'video',\r\n    data: {\r\n      label: 'Video Message',\r\n      type: 'video',\r\n      content: 'Watch this video',\r\n      mediaType: 'video',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  file: {\r\n    type: 'file',\r\n    data: {\r\n      label: 'File Message',\r\n      type: 'file',\r\n      content: 'Here is your file',\r\n      mediaType: 'file',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  fb_media: {\r\n    type: 'fb_media',\r\n    data: {\r\n      label: 'Facebook Media',\r\n      type: 'fb_media',\r\n      content: 'Facebook media content',\r\n      mediaType: 'image',\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  carousel: {\r\n    type: 'carousel',\r\n    data: {\r\n      label: 'Carousel',\r\n      type: 'carousel',\r\n      content: 'Browse our products',\r\n      metadata: {\r\n        items: []\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  ecommerce: {\r\n    type: 'ecommerce',\r\n    data: {\r\n      label: 'E-commerce',\r\n      type: 'ecommerce',\r\n      content: 'Shop now',\r\n      metadata: {\r\n        products: []\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  ai_reply: {\r\n    type: 'ai_reply',\r\n    data: {\r\n      label: 'AI Reply',\r\n      type: 'ai_reply',\r\n      content: 'AI-powered response',\r\n      metadata: {\r\n        prompt: '',\r\n        model: 'gpt-3.5-turbo'\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  condition: {\r\n    type: 'condition',\r\n    data: {\r\n      label: 'Condition',\r\n      type: 'condition',\r\n      metadata: {\r\n        condition: 'user_input',\r\n        operator: 'contains',\r\n        value: 'help'\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n  delay: {\r\n    type: 'delay',\r\n    data: {\r\n      label: 'Delay',\r\n      type: 'delay',\r\n      metadata: {\r\n        delay: 300\r\n      }\r\n    },\r\n    position: { x: 0, y: 0 },\r\n  },\r\n};\r\n","import * as React from \"react\"\r\nimport { Slot } from \"@radix-ui/react-slot\"\r\nimport { cva, type VariantProps } from \"class-variance-authority\"\r\nimport { cn } from \"../../lib/utils\"\r\n\r\nconst buttonVariants = cva(\r\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disable