{
  "name": "AI Website Chatbot \u2014 Main Handler",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "chat",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "node-webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -2752,
        240
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-gemini-key",
              "name": "gemini_api_key",
              "value": "YOUR_GEMINI_API_KEY",
              "type": "string"
            },
            {
              "id": "cfg-gemini-url",
              "name": "gemini_url",
              "value": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -2544,
        240
      ],
      "id": "node-config",
      "name": "Config"
    },
    {
      "parameters": {
        "jsCode": "const sessionId = $('Webhook').item.json.body.session_id;\nconst message = $('Webhook').item.json.body.message;\n\nif (!sessionId || !message || message.trim().length === 0) {\n  throw new Error('Missing session_id or message');\n}\n\nif (message.length > 2000) {\n  throw new Error('Message too long \u2014 max 2000 characters');\n}\n\n// Rate limiting \u2014 max 5 messages per 30 seconds per session\nconst now = Date.now();\nconst WINDOW_MS = 30 * 1000;\nconst MAX_MESSAGES = 5;\nconst CLEANUP_MS = 60 * 1000;\n\nconst workflowStaticData = $getWorkflowStaticData('global');\nif (!workflowStaticData.rateLimits) {\n  workflowStaticData.rateLimits = {};\n}\n\nfor (const sid of Object.keys(workflowStaticData.rateLimits)) {\n  workflowStaticData.rateLimits[sid] = workflowStaticData.rateLimits[sid]\n    .filter(ts => now - ts < CLEANUP_MS);\n  if (workflowStaticData.rateLimits[sid].length === 0) {\n    delete workflowStaticData.rateLimits[sid];\n  }\n}\n\nif (!workflowStaticData.rateLimits[sessionId]) {\n  workflowStaticData.rateLimits[sessionId] = [];\n}\n\nconst recentMessages = workflowStaticData.rateLimits[sessionId]\n  .filter(ts => now - ts < WINDOW_MS);\n\nif (recentMessages.length >= MAX_MESSAGES) {\n  throw new Error(\"You're sending messages too quickly. Please wait a moment and try again.\");\n}\n\nworkflowStaticData.rateLimits[sessionId].push(now);\n\nreturn {\n  json: {\n    session_id: sessionId,\n    message: message.trim()\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2336,
        240
      ],
      "id": "node-validate",
      "name": "Validate Input"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "assign-session",
              "name": "session_id",
              "value": "={{ $('Validate Input').item.json.session_id }}",
              "type": "string"
            },
            {
              "id": "assign-message",
              "name": "message",
              "value": "={{ $('Validate Input').item.json.message }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "node-setvars",
      "name": "Set Variables",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -2112,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "const sessionId = $('Set Variables').item.json.session_id;\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst session = workflowStaticData.sessions && workflowStaticData.sessions[sessionId];\nconst messages = session ? session.messages : [];\nreturn { json: { messages, session_id: sessionId } };"
      },
      "id": "node-recall",
      "name": "Recall Memory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1904,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "const sessionMessage = $('Set Variables').item.json.message;\nconst history = $('Recall Memory').item.json.messages || [];\n\n// ============================================================\n// CUSTOMIZE: Replace this system prompt with your own business\n// details, services, pricing, and conversation rules.\n// ============================================================\nconst systemPrompt = `You are the AI assistant for [YOUR COMPANY NAME]. You help visitors learn about your services, answer questions, and capture leads.\n\nCOMPANY DETAILS:\n- Company: [YOUR COMPANY NAME]\n- Phone: [YOUR PHONE NUMBER]\n- Email: [YOUR EMAIL]\n- Website: [YOUR WEBSITE URL]\n\nSERVICES:\n- [Service 1]: [Description and pricing]\n- [Service 2]: [Description and pricing]\n- [Service 3]: [Description and pricing]\n\nCONVERSATION RULES:\n1. Be professional, helpful, and concise.\n2. Naturally collect the user's name and email during conversation.\n3. Never fabricate information not listed above.\n4. Always recommend contacting the team for complex requirements.`;\n\n// Convert history to Gemini contents format\nconst contents = [];\nfor (const msg of history) {\n  contents.push({\n    role: msg.role === 'assistant' ? 'model' : 'user',\n    parts: [{ text: msg.content }]\n  });\n}\ncontents.push({ role: 'user', parts: [{ text: sessionMessage }] });\n\nreturn {\n  json: {\n    body: JSON.stringify({\n      systemInstruction: {\n        parts: [{ text: systemPrompt }]\n      },\n      contents: contents,\n      generationConfig: {\n        temperature: 0.7,\n        maxOutputTokens: 800\n      }\n    })\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1696,
        240
      ],
      "id": "node-buildreq",
      "name": "Build Request Body"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.gemini_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "x-goog-api-key",
              "value": "={{ $('Config').item.json.gemini_api_key }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ $json.body }}",
        "options": {}
      },
      "id": "node-gemini",
      "name": "Gemini API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1472,
        240
      ],
      "retryOnFail": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "check-ok",
              "leftValue": "={{ $json.candidates[0].content.parts[0].text }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -1264,
        240
      ],
      "id": "node-geminiok",
      "name": "Gemini OK?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "assign-reply",
              "name": "reply",
              "value": "={{ $json.candidates[0].content.parts[0].text }}",
              "type": "string"
            },
            {
              "id": "assign-session-out",
              "name": "session_id",
              "value": "={{ $('Set Variables').item.json.session_id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "node-extract",
      "name": "Extract Reply",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1040,
        240
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ reply: $('Extract Reply').item.json.reply, session_id: $('Set Variables').item.json.session_id }) }}",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "YOUR_WEBSITE_ORIGIN"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "POST, OPTIONS"
              },
              {
                "name": "Access-Control-Allow-Headers",
                "value": "Content-Type"
              },
              {
                "name": "Access-Control-Max-Age",
                "value": "86400"
              }
            ]
          }
        }
      },
      "id": "node-respond",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -608,
        288
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ reply: \"I'm having trouble right now. Please try again or contact us directly.\", session_id: $('Set Variables').item.json.session_id }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "YOUR_WEBSITE_ORIGIN"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "POST, OPTIONS"
              },
              {
                "name": "Access-Control-Allow-Headers",
                "value": "Content-Type"
              },
              {
                "name": "Access-Control-Max-Age",
                "value": "86400"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        -1040,
        496
      ],
      "id": "node-error",
      "name": "Error Response"
    },
    {
      "parameters": {
        "jsCode": "const sessionId = $('Set Variables').item.json.session_id;\nconst userMessage = $('Set Variables').item.json.message;\nconst assistantReply = $('Extract Reply').item.json.reply;\nconst now = Date.now();\nconst TTL_MS = 2 * 60 * 60 * 1000; // 2 hours\n\nconst workflowStaticData = $getWorkflowStaticData('global');\nif (!workflowStaticData.sessions) {\n  workflowStaticData.sessions = {};\n}\n\nif (!workflowStaticData.sessions[sessionId]) {\n  workflowStaticData.sessions[sessionId] = { messages: [], lastActive: now };\n}\n\nworkflowStaticData.sessions[sessionId].messages.push(\n  { role: 'user', content: userMessage },\n  { role: 'assistant', content: assistantReply }\n);\n\nif (workflowStaticData.sessions[sessionId].messages.length > 20) {\n  workflowStaticData.sessions[sessionId].messages =\n    workflowStaticData.sessions[sessionId].messages.slice(-20);\n}\n\nworkflowStaticData.sessions[sessionId].lastActive = now;\n\nfor (const sid of Object.keys(workflowStaticData.sessions)) {\n  const session = workflowStaticData.sessions[sid];\n  const lastActive = session.lastActive || 0;\n  if (now - lastActive > TTL_MS) {\n    delete workflowStaticData.sessions[sid];\n  }\n}\n\nreturn { json: { saved: true, session_id: sessionId } };"
      },
      "id": "node-savemem",
      "name": "Save Memory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -800,
        416
      ]
    },
    {
      "parameters": {
        "jsCode": "const userMessage = $('Set Variables').item.json.message;\nconst assistantReply = $('Extract Reply').item.json.reply;\nconst history = $('Recall Memory').item.json.messages || [];\n\nconst recentHistory = history.slice(-6);\n\nlet conversationText = '';\nfor (const msg of recentHistory) {\n  const role = msg.role === 'user' ? 'User' : 'Assistant';\n  conversationText += `${role}: ${msg.content}\\n`;\n}\nconversationText += `User: ${userMessage}\\nAssistant: ${assistantReply}`;\n\nreturn {\n  json: {\n    body: JSON.stringify({\n      systemInstruction: {\n        parts: [{ text: \"You are a lead detection assistant. Analyse the conversation and extract lead information if present.\\n\\nReturn ONLY valid JSON in this exact format, no explanation, no markdown:\\n{\\\"has_lead\\\": true/false, \\\"name\\\": \\\"...\\\", \\\"email\\\": \\\"...\\\", \\\"phone\\\": \\\"...\\\", \\\"intent\\\": \\\"...\\\"}\\n\\nSet has_lead to true only if you can identify at least an email address. Leave fields as empty string if not found.\" }]\n      },\n      contents: [\n        {\n          role: \"user\",\n          parts: [{ text: conversationText }]\n        }\n      ],\n      generationConfig: {\n        temperature: 0.1,\n        maxOutputTokens: 200,\n        responseMimeType: \"application/json\"\n      }\n    })\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -800,
        128
      ],
      "id": "node-buildlead",
      "name": "Build Lead Detection Body"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.gemini_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "x-goog-api-key",
              "value": "={{ $('Config').item.json.gemini_api_key }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ $json.body }}",
        "options": {}
      },
      "id": "node-leaddetect",
      "name": "Lead Detection",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -608,
        128
      ]
    },
    {
      "parameters": {
        "jsCode": "const raw = $input.item.json.candidates[0].content.parts[0].text;\nlet parsed;\ntry {\n  const cleaned = raw.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(cleaned);\n} catch(e) {\n  parsed = { has_lead: false, name: '', email: '', phone: '', intent: '' };\n}\nreturn { json: parsed };"
      },
      "id": "node-parselead",
      "name": "Parse Lead JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -416,
        128
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "conditions": [
            {
              "id": "check-lead",
              "leftValue": "={{ $json.has_lead }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "node-haslead",
      "name": "Has Lead?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -224,
        128
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ld-name",
              "name": "name",
              "value": "={{ $('Parse Lead JSON').item.json.name }}",
              "type": "string"
            },
            {
              "id": "ld-email",
              "name": "email",
              "value": "={{ $('Parse Lead JSON').item.json.email }}",
              "type": "string"
            },
            {
              "id": "ld-intent",
              "name": "intent",
              "value": "={{ $('Parse Lead JSON').item.json.intent }}",
              "type": "string"
            },
            {
              "id": "ld-session",
              "name": "session_id",
              "value": "={{ $('Set Variables').item.json.session_id }}",
              "type": "string"
            },
            {
              "id": "ld-phone",
              "name": "phone",
              "value": "={{ $('Parse Lead JSON').item.json.phone }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        0
      ],
      "id": "node-preplead",
      "name": "Prepare Lead Data"
    },
    {
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nif (!workflowStaticData.leads) {\n  workflowStaticData.leads = [];\n}\n\nconst lead = {\n  id: Date.now().toString(),\n  name: $input.item.json.name,\n  email: $input.item.json.email,\n  intent: $input.item.json.intent,\n  session_id: $input.item.json.session_id,\n  phone: $input.item.json.phone || '',\n  created_at: new Date().toISOString()\n};\n\nconst exists = workflowStaticData.leads.find(l => l.email === lead.email);\nif (!exists) {\n  workflowStaticData.leads.push(lead);\n}\n\nreturn { json: { success: true, lead_id: lead.id, is_duplicate: !!exists } };"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        208,
        0
      ],
      "id": "node-savelead",
      "name": "Save Lead to Store"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SUPABASE_URL/rest/v1/leads",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "apikey",
              "value": "YOUR_SUPABASE_ANON_KEY"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_SUPABASE_ANON_KEY"
            },
            {
              "name": "Prefer",
              "value": "resolution=ignore-duplicates"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ name: $('Prepare Lead Data').item.json.name, email: $('Prepare Lead Data').item.json.email, phone: $('Prepare Lead Data').item.json.phone, intent: $('Prepare Lead Data').item.json.intent, session_id: $('Prepare Lead Data').item.json.session_id }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node-supabase",
      "name": "Save to Supabase",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        448,
        0
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "YOUR_CRM_FORM_URL",
        "options": {}
      },
      "id": "node-fetchcrm",
      "name": "Fetch CRM Form",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        0
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Extract CSRF token from the CRM form HTML\nconst html = $input.item.json.data || '';\nconst csrfMatch = html.match(/\"csrf_token_name\":\"([a-f0-9]+)\"/);\nconst csrfToken = csrfMatch ? csrfMatch[1] : '';\nconst key = 'YOUR_CRM_FORM_KEY';\n\nif (!csrfToken) {\n  throw new Error('Could not extract CSRF token from CRM form');\n}\n\nreturn {\n  json: {\n    csrf_token: csrfToken,\n    key: key,\n    name: $('Prepare Lead Data').item.json.name,\n    email: $('Prepare Lead Data').item.json.email,\n    phonenumber: $('Prepare Lead Data').item.json.phone,\n    description: 'AI Chatbot Lead | Intent: ' + $('Prepare Lead Data').item.json.intent + ' | Session: ' + $('Prepare Lead Data').item.json.session_id\n  }\n};"
      },
      "id": "node-csrf",
      "name": "Extract CSRF Token",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        928,
        0
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_CRM_FORM_URL",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/x-www-form-urlencoded"
            },
            {
              "name": "Origin",
              "value": "YOUR_WEBSITE_ORIGIN"
            },
            {
              "name": "Referer",
              "value": "YOUR_CRM_FORM_URL"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "csrf_token_name",
              "value": "={{ $json.csrf_token }}"
            },
            {
              "name": "key",
              "value": "={{ $json.key }}"
            },
            {
              "name": "name",
              "value": "={{ $json.name }}"
            },
            {
              "name": "email",
              "value": "={{ $json.email }}"
            },
            {
              "name": "phonenumber",
              "value": "={{ $json.phonenumber }}"
            },
            {
              "name": "description",
              "value": "={{ $json.description }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node-submitcrm",
      "name": "Submit to CRM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1168,
        0
      ],
      "continueOnFail": true
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Validate Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Input": {
      "main": [
        [
          {
            "node": "Set Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Variables": {
      "main": [
        [
          {
            "node": "Recall Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recall Memory": {
      "main": [
        [
          {
            "node": "Build Request Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Request Body": {
      "main": [
        [
          {
            "node": "Gemini API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini API": {
      "main": [
        [
          {
            "node": "Gemini OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini OK?": {
      "main": [
        [
          {
            "node": "Extract Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Reply": {
      "main": [
        [
          {
            "node": "Save Memory",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Lead Detection Body",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Lead Detection Body": {
      "main": [
        [
          {
            "node": "Lead Detection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Detection": {
      "main": [
        [
          {
            "node": "Parse Lead JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Lead JSON": {
      "main": [
        [
          {
            "node": "Has Lead?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Lead?": {
      "main": [
        [
          {
            "node": "Prepare Lead Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Lead Data": {
      "main": [
        [
          {
            "node": "Save Lead to Store",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Lead to Store": {
      "main": [
        [
          {
            "node": "Save to Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Supabase": {
      "main": [
        [
          {
            "node": "Fetch CRM Form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch CRM Form": {
      "main": [
        [
          {
            "node": "Extract CSRF Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract CSRF Token": {
      "main": [
        [
          {
            "node": "Submit to CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "AI"
    },
    {
      "name": "Chatbot"
    },
    {
      "name": "Lead Generation"
    }
  ]
}