UNPKG

@webdevarif/reactflow

Version:

Reusable ReactFlow components for social media bot automation

1 lines 87.5 kB
{"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