Skip to content

Instantly share code, notes, and snippets.

@BananaAcid
Last active May 25, 2025 13:54
Show Gist options
  • Save BananaAcid/42d70af44ae5e52f4a195affef855acc to your computer and use it in GitHub Desktop.
Save BananaAcid/42d70af44ae5e52f4a195affef855acc to your computer and use it in GitHub Desktop.
n8n Chat as workflow - Blueprint
{
"nodes": [
{
"parameters": {
"content": "## Chat WebApp in vueEngine\n## with Vue3 in PARTS\n- with routes\n- with real session\n- uses JWT for session data\n- with login auth\n- with re-login by JWT\n\n\nโ„น๏ธ **Note**\nno login token or alike needed by any request, like any WebApp/WebSite\n\n\nโš ๏ธ **SETUP**\n1. You need to set a passphrase for JWT nodes (it must be the same for each one).\n2. You need to configure the AI Agent node with the needed API key.",
"height": 660,
"width": 280
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-660,
3840
],
"typeVersion": 1,
"id": "9c071704-c523-49a1-98ea-9c4ff9c86daf",
"name": "Sticky Note23"
},
{
"parameters": {
"content": "## Server\n*Webhooks contain url*\n1. root url must end in `/`",
"height": 520,
"width": 660
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
3840
],
"typeVersion": 1,
"id": "8442a23d-7316-4ae6-9339-47d7354d8f5b",
"name": "Sticky Note24"
},
{
"parameters": {
"content": "## Vue Pages and components\n1. HTML Generator has not \"Tag\" or optional props, we need to add filename and type as JSON\n2. the pages have to be triggered, otherwise they will not output content\n - added benefit: access to webhook data\n3. `{{ '{{data}'+'}' }}` has to be used for Vue vars, since n8n uses mustache as well",
"height": 1020,
"width": 440
},
"type": "n8n-nodes-base.stickyNote",
"position": [
320,
3840
],
"typeVersion": 1,
"id": "79ea64c6-c877-4c01-ac8d-45ca72874685",
"name": "Sticky Note25"
},
{
"parameters": {
"content": "## magic-less combining\n1. Number 1 must be the webhook",
"height": 660,
"width": 400
},
"type": "n8n-nodes-base.stickyNote",
"position": [
780,
3840
],
"typeVersion": 1,
"id": "3ef51743-4c02-460c-ac27-ee08652ee897",
"name": "Sticky Note26"
},
{
"parameters": {
"content": "## Index page\n*vueEngine configuration*\n1. Needed option: Execute Once (because `$input.all()` is used)",
"height": 660,
"width": 180
},
"type": "n8n-nodes-base.stickyNote",
"position": [
1200,
3840
],
"typeVersion": 1,
"id": "579ddc74-27d9-42f3-ae19-b84d41878d20",
"name": "Sticky Note27"
},
{
"parameters": {
"content": "## output\n1. `Response Body` must be `{{$json.html}}`",
"height": 660,
"width": 180
},
"type": "n8n-nodes-base.stickyNote",
"position": [
1400,
3840
],
"typeVersion": 1,
"id": "f56007f3-38dd-42dd-9f32-10d07e4cd748",
"name": "Sticky Note28"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"type\": \"app\",\n \"id\": \"App.vue\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
600,
4120
],
"id": "d725051b-14d9-42e3-9fbd-bf6a00ddf47b",
"name": "Add 'type=app', 'id=App.vue'2"
},
{
"parameters": {
"content": "## Server REST API\n",
"height": 480,
"width": 660
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
4380
],
"typeVersion": 1,
"id": "b4bf980f-03ab-4e06-9b2c-b2879d824f91",
"name": "Sticky Note30"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.1,
"position": [
1020,
4060
],
"id": "02be7d00-59ca-440f-b024-6c6eea6e7b51",
"name": "Combine server data and vue files2",
"notesInFlow": true,
"notes": "1. webhook, 2. vue files\n\nwebhook data is provided as `globalThis.N8N_DATA` to the app"
},
{
"parameters": {
"jsCode": "// Database mock data\n// โš ๏ธ Do NOT save cleartext password!\nreturn {\n users: [\n {\n id: 0,\n email: '[email protected]',\n username: 'Testoooorrr',\n name: {first: 'Abc', last: 'Xyz'},\n loginType: 'email',\n enabled: true,\n password_hash: '69e73f4c'\n },\n {\n id: 1,\n email: '[email protected]',\n username: 'Testinaaa',\n name: {first: 'Bcd', last: 'Wxy'},\n loginType: 'email',\n enabled: true, \n password_hash: '69f49189'\n },\n ],\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
820,
5180
],
"id": "0413b49b-3c3b-4927-9d34-871bf4486af0",
"name": "FakeDB",
"notesInFlow": false,
"notes": "Fake DB"
},
{
"parameters": {
"jsCode": "/**\n * This is not not the BEST way to handle password verification, but the best without\n * needing the crypto/bcrypt module (which would need a custom node or would need to be enabled in n8n config.)\n */\n\n// https://medium.com/@khorvath3327/implementing-a-hashing-algorithm-in-node-js-9bbe56caab28\nvar djb2_better = function (string) {\n var h = 5831 << 2;\n var i = 0;\n for (i = 0; i < string.length; i++) {\n var ascii = string.charCodeAt(i);\n h = ((h << 3) ^ h) ^ ascii;\n }\n return (h & 0xffffffffff).toString(16);\n}\n\n\nconst item = $input.first();\nconst email = item.json.body?.email;\nconst pass = item.json.body?.password;\n// we only use password_hash if it is set by a node prior, not if it is from the request.body\nconst hash = item.json.password_hash || (pass?.trim() ? djb2_better(pass) : undefined);\n\nlet users = $('FakeDB').first().json.users;\n\n//debug\n//let all = { login: {email, pass, 'pw is hash': hash}, users: users.map(u => ({...u, h: djb2_better(u.password), v_p: u.password_hash == hash, v_e: u.email == email, /* v_e_:{'u.email':u.email, 'l.email': email},*/ })) };\nlet all;\n\n\n// get user\nlet user = users.find(row => row.email == email && row.password_hash == hash);\n\n// create JWT with login status\nlet jwtData = {\n user,\n info: {\n isLoggedin: !!user,\n }\n}\n\n\nreturn {\n jwtData,\n user,\n all,\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1160,
5020
],
"id": "df729612-41de-4264-8697-4736cd277df3",
"name": "FakeDB-Authentication",
"retryOnFail": false,
"notesInFlow": true,
"notes": "return {user: object}"
},
{
"parameters": {
"content": "## Auth",
"height": 420,
"width": 1000
},
"type": "n8n-nodes-base.stickyNote",
"position": [
580,
4960
],
"typeVersion": 1,
"id": "1beb9b41-c020-4c62-bad4-bfea7682611a",
"name": "Sticky Note33"
},
{
"parameters": {
"useJson": true,
"claimsJson": "={{ $json.jwtData }}",
"options": {}
},
"type": "n8n-nodes-base.jwt",
"typeVersion": 1,
"position": [
1400,
5180
],
"id": "34120734-1ba9-44fd-a4bd-19e961838d9d",
"name": "JWT",
"credentials": {
"jwtAuth": {
"id": "hhCF430EVcJSsqJs",
"name": "JWT Auth account"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c9c2c9bc-745e-4c54-ab6f-8965cbe812ba",
"name": "user",
"value": "={{ $('FakeDB-Authentication').item.json.user }}",
"type": "object"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1400,
5020
],
"id": "b2f34327-db47-4821-9cff-ea2d857308af",
"name": "Edit Fields"
},
{
"parameters": {
"operation": "decode",
"token": "={{ $json.headers.cookie.matchAll(/([^;=\\s]*)=([^;]*)/g).toArray().find(c => c[1] == 'SESSID_VueEngine_token')?.reduce((acc, v, k) => k == 2 ? decodeURIComponent(v) : acc, '') || ''}}",
"options": {}
},
"type": "n8n-nodes-base.jwt",
"typeVersion": 1,
"position": [
-80,
5760
],
"id": "13617336-769d-4c12-9529-7f97c1615b01",
"name": "JWT from Session",
"alwaysOutputData": false,
"credentials": {
"jwtAuth": {
"id": "hhCF430EVcJSsqJs",
"name": "JWT Auth account"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"content": "## Server REST API: Chat Endpoint\n*reuseable template for login check*\nrequires: `{ model: string, chatInput: string }`",
"height": 520,
"width": 2180
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
5580
],
"typeVersion": 1,
"id": "ddfa6279-76a5-41db-8bad-bde11c2842d4",
"name": "Sticky Note34"
},
{
"parameters": {
"content": "## Server REST API: Auth Endpoint\nrequires: `{ email: string, password: string }` OR active `Session`\nOR: if no user login is provided, tries to use JWT",
"height": 680,
"width": 2180
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
4880
],
"typeVersion": 1,
"id": "6a4da097-580f-483c-8b28-e2a7aebf4180",
"name": "Sticky Note35"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.1,
"position": [
1000,
5020
],
"id": "6a93347f-589a-435c-b87d-3480494af4c9",
"name": "Auth Merge",
"notesInFlow": true,
"notes": "1. Auth Params, 2. DB"
},
{
"parameters": {
"useJson": true,
"claimsJson": "={{ $('JWT from Session').first().json.payload }}",
"options": {}
},
"type": "n8n-nodes-base.jwt",
"typeVersion": 1,
"position": [
1240,
5940
],
"id": "091f85c5-b366-4491-a7bb-3988e4c3885c",
"name": "JWT token update",
"notesInFlow": true,
"credentials": {
"jwtAuth": {
"id": "hhCF430EVcJSsqJs",
"name": "JWT Auth account"
}
},
"notes": "โš ๏ธ always use last changed payload node โš ๏ธ"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($input.first().json) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Set-Cookie",
"value": "=SESSID_VueEngine_token={{ $('JWT').item.json.token }}; expires={{ (new Date(Date.now() + 1*24*60*60*1000)).toUTCString() }}; path=/;"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.2,
"position": [
1660,
5020
],
"id": "f96a5131-fcaf-480c-9ab6-99e93100d7ff",
"name": "Respond with user data",
"retryOnFail": true,
"notesInFlow": true,
"notes": "and activates session"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($input.first().json ) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Set-Cookie",
"value": "=SESSID_VueEngine_token={{ $input.last()?.json?.token ?? '' }}; expires={{ (new Date(Date.now() + 1*24*60*60*1000)).toUTCString() }}; path=/;"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.2,
"position": [
1660,
5640
],
"id": "74f90b6f-dbcf-4340-940d-b6b7a19569cc",
"name": "Respond with outputs",
"executeOnce": true,
"notesInFlow": true,
"notes": "and refreshes session"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"method\": \"GET\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-80,
4040
],
"id": "dae44446-c6f2-46a1-9212-f2ddecddba1d",
"name": "Add `method=GET`",
"notesInFlow": true,
"notes": "add to `N8N_DATA`"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"method\": \"POST\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-80,
4200
],
"id": "43cb4605-a4d3-46a8-9678-af837c91ff22",
"name": "Add `method=POST`",
"notesInFlow": true,
"notes": "add to `N8N_DATA`"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"error\": \"Server login validation error\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
5640
],
"id": "941c160d-663c-444c-bd2c-1e81e1673a6f",
"name": "Error message",
"notesInFlow": true,
"notes": "\"Login validation error\""
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.1,
"position": [
1480,
5740
],
"id": "110fcf67-a2b0-4953-98b1-98d764c1434d",
"name": "Make output error save",
"notesInFlow": true,
"notes": "for response to be able to take $input"
},
{
"parameters": {
"content": "## Content\nThis part is the unique to the chat",
"height": 480,
"width": 680
},
"type": "n8n-nodes-base.stickyNote",
"position": [
400,
5600
],
"typeVersion": 1,
"id": "580da69a-1cae-4324-8ef1-f4876bc9b5a4",
"name": "Sticky Note36"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
620,
5020
],
"id": "db6b9255-ee55-412c-9084-bd972cc60a00",
"name": "Activate Nodes"
},
{
"parameters": {
"httpMethod": "POST",
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/models.json",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-300,
4640
],
"id": "d391db97-69be-4b22-b5c9-029d647e3b55",
"name": "๐ŸŒ/chat/models.json",
"webhookId": "3fa53bdc-7816-4826-a9ae-4a547ef5b7fa",
"notesInFlow": true,
"notes": "REST: get OpenAI-API models"
},
{
"parameters": {
"httpMethod": "POST",
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/auth",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-280,
5000
],
"id": "65263368-96a6-484d-8885-658ac36788ce",
"name": "๐ŸŒ/chat/auth",
"webhookId": "79415455-a15a-46bc-8091-8a1719a99283"
},
{
"parameters": {
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-300,
4040
],
"id": "ca9f7e75-9712-4b4d-94fc-9ce671dcbc48",
"name": "๐ŸŒGET /chat/",
"webhookId": "4f0366ac-982d-483e-8dba-072b3b01fb48"
},
{
"parameters": {
"httpMethod": "POST",
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-300,
4200
],
"id": "7fbedb10-d7f1-4cf1-8a98-fc271c240ee9",
"name": "๐ŸŒPOST /chat/",
"webhookId": "3fa53bdc-7816-4826-a9ae-4a547ef5b7fa"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ff54fae7-b4b8-4cf1-89b2-dec766ea6e8b",
"leftValue": "={{ $json.payload.info.isLoggedin }}",
"rightValue": "true",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-80,
5940
],
"id": "d2fa7ab3-c769-461b-b440-f66185b03113",
"name": "If isLoggedIn"
},
{
"parameters": {
"httpMethod": "POST",
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/endpoint.json",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-300,
5760
],
"id": "613c059f-41a3-42ac-98e2-a80ffd8b360f",
"name": "๐ŸŒ/chat/endpoint.json",
"webhookId": "79415455-a15a-46bc-8091-8a1719a99283",
"notesInFlow": false
},
{
"parameters": {
"mode": "combineBySql",
"numberInputs": 3
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.1,
"position": [
1240,
5740
],
"id": "5f41c4bb-10ac-4fa0-977c-a623028e56e7",
"name": "Output collector",
"notesInFlow": true,
"notes": "all items keys into one item"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "8e179d53-fa76-410a-b09c-a7c60e2bb7cf",
"name": "=body.email",
"value": "={{ $json.payload.user.email }}",
"type": "string"
},
{
"id": "708c2634-3766-43bf-9009-4a639d46a6d6",
"name": "=password_hash",
"value": "={{ $json.payload.user.password_hash }}",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
300,
5360
],
"id": "00eb42c8-2d8b-4082-8bdf-fec6c50498f0",
"name": "map JWT login creds",
"notesInFlow": true,
"notes": "and store `password_hash`"
},
{
"parameters": {
"content": "## Auto login with JWT",
"height": 580,
"width": 300
},
"type": "n8n-nodes-base.stickyNote",
"position": [
220,
4960
],
"typeVersion": 1,
"id": "edd51490-3430-4bbd-84e8-78772550d935",
"name": "Sticky Note37"
},
{
"parameters": {
"content": "## DB",
"height": 360,
"width": 520,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"position": [
780,
5000
],
"typeVersion": 1,
"id": "87c09178-c78d-465c-b314-61b4341336bc",
"name": "Sticky Note38"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ff54fae7-b4b8-4cf1-89b2-dec766ea6e8b",
"leftValue": "={{ $json.body.email }}",
"rightValue": "true",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "abff7183-a837-4875-aa1e-d0ff93dc5ae5",
"leftValue": "={{ $json.body.password }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
300,
5040
],
"id": "28f78378-4a85-4cc9-af40-51c4c1fdd886",
"name": "If body: user & password"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
140,
4040
],
"id": "3dee75dc-ca96-4ad8-b001-1730900b0012",
"name": "Combine to work for GET & POST"
},
{
"parameters": {
"html": "<template lang=\"pug\">\n //- trying out PUG support\n VContainer(v-if=\"!loggedInUser?.data\")\n VRow(justify=\"center\")\n VCol(cols=\"12\" sm=\"8\" md=\"6\")\n VCard\n VCardTitle.headline Login\n VCardText\n VForm(ref=\"form\" v-model=\"valid\" lazy-validation)\n VTextField(\n @keypress.enter=\"doLogin\"\n v-model=\"email\"\n label=\"E-Mail\"\n required\n :rules=\"emailRules\"\n autocomplete=\"username\"\n )\n VTextField(\n @keypress.enter=\"doLogin\"\n v-model=\"password\"\n label=\"Passwort\"\n type=\"password\"\n required\n :rules=\"passwordRules\"\n autocomplete=\"current-password\"\n )\n VAlert(v-if=\"error\" :text=\"error\" type=\"error\" density=\"compact\")\n\n VCardActions\n VBtn(\n color=\"primary\"\n @click=\"doLogin\"\n :disabled=\"!valid || loadingUser.state\"\n block\n type=\"submit\"\n )\n | login\n span.loader(v-if=\"loadingUser.state\") ๐ŸŸข\n //-\n VProgressCircular(\n v-if=\"loadingUser.state\"\n indeterminate\n color=\"white\"\n size=\"20\"\n width=\"2\"\n class=\"ml-2\"\n )\n</template>\n\n<script setup>\n import { ref, reactive } from 'vue';\n\n // init a \"store\" alike\n let loggedInUser = globalThis.loggedInUser || ( globalThis.loggedInUser = reactive({ data: null }));\n let loadingUser = globalThis.loadingUser || ( globalThis.loadingUser = reactive({state: false}));\n let urlMode = globalThis.urlMode || ( globalThis.urlMode = reactive({ data: '' }));\n\n const error = ref('');\n const valid = ref(true);\n const email = ref('');\n const password = ref('');\n const emailRules = [\n v => !!v || 'E-Mail ist erforderlich',\n v => /.+@.+\\..+/.test(v) || 'E-Mail muss gรผltig sein',\n ];\n const passwordRules = [\n v => !!v || 'Passwort ist erforderlich',\n v => (v && v.length >= 6) || 'Passwort muss mindestens 6 Zeichen lang sein',\n ];\n\n \n async function login(creds = null) {\n loadingUser.state = true;\n error.value = '';\n\n let url = globalThis.N8N_DATA?.webhookUrl?.replace(/\\/webhook.*?\\//, `/webhook${urlMode.data}/`);\n let endpoint = (url || './') + 'auth';\n \n let p = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(\n creds ?? {}\n /*{\n email: '[email protected]',\n password: 'abc123',\n }*/\n )\n })\n .then(d => d.json())\n .catch(err => { console.error(err); return {err}; } );\n\n loadingUser.state = false;\n\n if ('user' in p) {\n loggedInUser.data = p.user;\n \n if (creds && !p.user)\n error.value = 'Invalid credentials';\n }\n else \n error.value = p.err?.message || p.err?.error/* -> n8n*/ || p.error /*n8n*/ || ( /*n8n webhook error*/ p.message ? p.message + '\\n' + p.hint : null) || 'Unknonwn Error';\n \n return p;\n }\n\n async function doLogin() {\n let result = await login(\n {\n email: email.value,\n password: password.value,\n }\n );\n \n if (result.error) alert(result.error.message);\n }\n\n // try autologin\n login();\n \n</script>\n\n\n<style lang=\"less\" scoped>\n .page {\n background-color: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n max-width: 800px;\n margin: 0 auto;\n\n @color: #34495e;\n h1 {\n color: #2c3e50;\n margin-bottom: 1rem;\n }\n p {\n color: @color;\n line-height: 1.6;\n }\n }\n\n *:deep(ul), *:deep(li) {\n margin: unset;\n padding: unset;\n }\n</style>\n\n<style> \n #output ul, #output li {\n margin: unset !important;\n padding: unset !important;\n }\n</style>"
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
400,
4300
],
"id": "caeaee10-520b-4018-aff6-e268ae1e3d91",
"name": "HTML: /components/UserLogin.vue"
},
{
"parameters": {
"html": "<template>\n <div class=\"page\">\n <h1>Chat with <span v-html=\"selectedModel.pretty ?? 'No model selected'\" /></h1>\n\n <div class=\"chat-container\">\n <div\n v-for=\"(message, index) in messages\"\n :key=\"index\"\n :class=\"['message-row', message.type]\"\n >\n <div :class=\"['message-bubble', message.type]\">\n <template v-if=\"message.type === 'ai'\">\n <span v-html=\"message.content\"></span>\n </template>\n <template v-else>\n <span v-html=\"message.content\"></span>\n </template>\n </div>\n </div>\n </div>\n\n <div class=\"input-area\">\n <input v-model=\"chatText\" @keypress.enter=\"handleSend\" placeholder=\"Type your message...\" />\n <VBtn @click=\"handleSend\" :disabled=\"!chatText.trim()\">send</VBtn>\n </div>\n\n <ModelSelection />\n\n </div>\n</template>\n\n<script setup>\n import { ref, watch, reactive, nextTick } from 'vue';\n import { marked } from 'marked';\n\n // init a \"store\" alike\n let selectedModel = globalThis.selectedModel || ( globalThis.selectedModel = reactive({ id: '', pretty: '' }));\n let urlMode = globalThis.urlMode || ( globalThis.urlMode = reactive({ data: '' }));\n\n let chatText = ref('Hi');\n let output = ref('');\n let error = ref('');\n\n async function send(chatInput) {\n\n let url = globalThis.N8N_DATA?.webhookUrl?.replace(/\\/webhook.*?\\//, `/webhook${urlMode.data}/`);\n let endpoint = (url || './') + 'endpoint.json';\n \n let x = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: selectedModel.id,\n chatInput: chatInput, \n })\n })\n .then(d => d.json())\n .catch(err => { console.error(err); return {err}; } );\n\n console.log('api result', x);\n\n output.value = '';\n error.value = '';\n\n if ('output' in x)\n output.value = marked.parse(x.output);\n else \n error.value = x.err?.message || x.err?.error/* -> n8n*/ || x.error /*n8n*/ || ( /*n8n webhook error*/ x.message ? x.message + '\\n' + x.hint : null) || 'Unknonwn Error';\n }\n\n const messages = ref([]);\n\n watch(output, (newOutput, oldOutput) => {\n if (typeof newOutput === 'string' && newOutput.trim() !== '' && newOutput !== oldOutput) {\n messages.value.push({ type: 'ai', content: newOutput });\n scrollToBottom();\n }\n });\n\n watch(error, (newError, oldError) => {\n if (typeof newError === 'string' && newError.trim() !== '' && newError !== oldError) {\n messages.value.push({ type: 'error', content: newError });\n scrollToBottom();\n }\n });\n\n watch(() => selectedModel.pretty, (newId, oldId) => {\n const oldIdDisplay = String(oldId ?? 'None');\n const newIdDisplay = String(newId ?? 'None');\n\n if (newId === oldId || (oldId === undefined && (newId === '' || newId === null || newId === undefined))) {\n return;\n }\n\n if (oldId === undefined) {\n messages.value.push({ type: 'info', content: `Initial model: ${newIdDisplay}` });\n } else {\n messages.value.push({ type: 'info', content: `Model changed from ${oldIdDisplay} to ${newIdDisplay}` });\n }\n\n scrollToBottom();\n }, { immediate: true }); // Run immediately to show initial model\n \n async function handleSend() {\n const userMessage = chatText.value.trim();\n\n if (userMessage) {\n messages.value.push({ type: 'user', content: userMessage });\n chatText.value = '';\n scrollToBottom();\n await send(userMessage);\n }\n };\n\n function scrollToBottom() {\n nextTick(() => {\n const container = document.querySelector('.chat-container');\n if (container) {\n setTimeout(() => {\n container.scrollTop = container.scrollHeight;\n }, 0);\n }\n });\n }\n\n if (messages.value.length === 0) {\n messages.value.push({ type: 'info', content: 'Welcome to the chat!' });\n }\n</script>\n\n<style lang=\"less\" scoped>\n .page {\n background-color: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n max-width: 800px;\n margin: 20px auto;\n display: flex;\n flex-direction: column;\n // fix height to keep chatbar visible\n // full Viewport-height - AppBar (ca. 64px) - top/bottom margin/padding\n min-height: calc(100vh - 40px - 1.5*64px);\n max-height: calc(100vh - 40px - 1.5*64px);\n overflow: hidden; // Verhindert, dass Inhalt auรŸerhalb des page div รผberlรคuft\n\n @color: #34495e;\n h1 {\n color: #2c3e50;\n margin-bottom: 1rem;\n font-size: 1.5rem; // Etwas kleiner fรผr besseren Flow\n }\n p {\n color: @color;\n line-height: 1.6;\n margin-bottom: 10px;\n }\n }\n\n // Container for the chat bubbles\n .chat-container {\n flex-grow: 1;\n overflow-y: auto;\n padding: 10px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n background-color: #e5ddd5;\n box-shadow: 0 0 2px 1px #b5b5b5;\n border-radius: 8px;\n margin-bottom: 15px;\n }\n\n // Row for each message (used for alignment)\n .message-row {\n display: flex;\n &.user {\n justify-content: flex-end;\n }\n &.ai {\n justify-content: flex-start;\n }\n &.error, &.info {\n justify-content: center;\n text-align: center;\n }\n }\n\n // The message bubble itself\n .message-bubble {\n max-width: 80%;\n padding: 8px 12px;\n border-radius: 18px;\n word-wrap: break-word;\n word-break: break-word;\n font-size: 0.95rem;\n line-height: 1.4;\n box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.1);\n\n &.user {\n background-color: #dcf8c6;\n color: #000;\n border-bottom-right-radius: 2px;\n }\n &.ai {\n background-color: #fff;\n color: #000;\n border-bottom-left-radius: 2px;\n }\n &.error {\n background-color: #f8d7da;\n color: #721c24;\n font-weight: bold;\n font-size: 0.9rem;\n border-radius: 8px;\n }\n &.info {\n background-color: #fff3cd;\n color: #856404;\n font-style: italic;\n font-size: 0.9rem;\n border-radius: 8px;\n }\n\n // Style for Markdown in AI Bubbles\n *:deep(p) { margin: unset; padding: unset; }\n *:deep(ul), *:deep(ol) { margin: unset; padding-left: 20px; }\n *:deep(li) { margin: unset; padding: unset; }\n }\n\n // Input area styles\n .input-area {\n display: flex;\n align-items: center;\n gap: 10px; // space between Input and Button\n padding-top: 0; // space above Input area\n //border-top: 1px solid #eee; // line\n margin-bottom: 1em;\n\n input {\n flex-grow: 1; // Input needs the most space\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 20px;\n outline: none;\n font-size: 1rem;\n\n &:focus {\n border-color: #007bff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n }\n }\n\n \n }\n\n</style>"
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
400,
4480
],
"id": "6debc46c-f92b-4dfb-92eb-53752f4396a6",
"name": "HTML: /Home.vue"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"type\": \"page\",\n \"id\": \"Home.vue\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
600,
4480
],
"id": "d5ecb04e-c745-412c-a407-ea211b123cc3",
"name": "Add 'type=page', 'id=Home.vue'"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"type\": \"component\",\n \"id\": \"UserLogin.vue\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
600,
4300
],
"id": "d38eeb57-1717-434c-9128-8dc8123cb226",
"name": "Add 'type=component', 'id=UserLogin.vue'"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.html }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "X-Server",
"value": "vueEngine in n8n"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.2,
"position": [
1440,
4060
],
"id": "5eeaefbb-e99e-42ed-8209-9b4e84c09707",
"name": "Respond with WebApp"
},
{
"parameters": {
"html": "<template>\n <VCard class=\"pa-2\">\n <p>Models by this OpenAI-API</p>\n \n <VAutocomplete :items=\"data\" density=\"compact\" v-model=\"selectedModel.id\" />\n <small><a href @click=\"getModels\">refresh</a></small>\n\n <!-- details>\n <summary>Model details</summary>\n model (+pretty):\n <pre v-html=\"error || JSON.stringify(models[selectedModel.id], null, 2)\"></pre>\n selectedModel:\n <pre v-html=\"JSON.stringify(selectedModel, null, 2)\"></pre>\n </details -->\n </VCard>\n</template>\n\n<style scoped>\n</style>\n\n<script setup>\n import { ref, onMounted, watch, reactive } from 'vue';\n \n let selectedModel = globalThis.selectedModel || ( globalThis.selectedModel = reactive({ id: '', pretty: '' }));\n let urlMode = globalThis.urlMode || ( globalThis.urlMode = reactive({ data: '' }));\n \n let data = ref([]);\n let models = ref([]);\n\n // prefill\n if (!selectedModel.id) {\n selectedModel.id = 'models/gemini-2.0-flash';\n selectedModel.pretty = 'gemini-2.0-flash';\n }\n\n async function getModels(ev = undefined) {\n if (ev) ev.preventDefault();\n \n let url = globalThis.N8N_DATA?.webhookUrl?.replace(/\\/webhook.*?\\//, `/webhook${urlMode.data}/`);\n let endpoint = (url || './') + 'models.json';\n \n let items = await fetch(endpoint, {method: 'POST'})\n .then(d => d.json()).then(d => d.data)\n .catch(e => {console.error(e); return [];});\n\n if (!items) {\n models.value = [];\n data.value = [];\n selectedModel.id = '';\n return;\n }\n //console.log('items', items);\n models.value = items.reduce( (acc, el) => { acc[el.id] = {...el, pretty: el.id.split('/').pop()}; return acc; }, {} );\n data.value = items.map(({ id }) => ({title: id.split('/').pop(), value: id}));\n }\n \n onMounted(async _ => {\n await getModels();\n watch(() => selectedModel.id, val => selectedModel.pretty = models.value[selectedModel.id]?.pretty, { immediate: true });\n });\n</script>\n\n<style lang=\"less\" scoped>\n .v-card {\n flex-shrink: 0;\n border-top: 1px solid #888;\n }\n</style>"
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
400,
4660
],
"id": "ca5d0611-fbdb-4649-9c26-3d99d6a2bfd2",
"name": "HTML: /components/ModelSelection.vue"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"type\": \"component\",\n \"id\": \"ModelSelection.vue\"\n}\n",
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
600,
4660
],
"id": "a27511cc-417f-4128-a6bd-d369b25291c9",
"name": "Add 'type=component', 'id=ModelSelection.vue'"
},
{
"parameters": {
"content": "## JWT auth check",
"height": 400
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-140,
5680
],
"typeVersion": 1,
"id": "fc6d5314-0a0d-4a75-b446-0895cf455053",
"name": "Sticky Note29"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "200b453b-efea-4160-8703-a4024e67f42a",
"name": "payload.sessionId",
"value": "={{ $json.payload.sessionId || (+new Date).toString(36).slice(-5) + Math.random().toString(36).substr(2, 5) }}",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "82929dc6-156d-4969-8bb6-a315111db6d7",
"name": "Make payload have a `sessionId`",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
220,
5800
],
"notesInFlow": true,
"notes": "Get `sessionId` or create one"
},
{
"parameters": {
"content": "### โš ๏ธ Test user login session\n... for calling in browser",
"height": 400,
"width": 500,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-340,
5140
],
"typeVersion": 1,
"id": "dc42ff60-8dcd-457b-8456-77673eb1dc17",
"name": "Sticky Note39"
},
{
"parameters": {
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/auth/backdoor2",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-280,
5380
],
"id": "1a45a393-e41d-410f-9b04-5721a4034df2",
"name": "๐ŸŒ/chat/auth/backdoor2",
"webhookId": "79415455-a15a-46bc-8091-8a1719a99283",
"notesInFlow": true,
"notes": "Test with login creds"
},
{
"parameters": {
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/auth/backdoor",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-280,
5220
],
"id": "83ae1967-0548-4b5c-b53d-1726da4f5099",
"name": "๐ŸŒ/chat/auth/backdoor",
"webhookId": "79415455-a15a-46bc-8091-8a1719a99283",
"notesInFlow": true,
"notes": "Test with having a JWT session"
},
{
"parameters": {
"operation": "decode",
"token": "={{ $json.headers.cookie.matchAll(/([^;=\\s]*)=([^;]*)/g).toArray().find(c => c[1] == 'SESSID_VueEngine_token')?.reduce((acc, v, k) => k == 2 ? decodeURIComponent(v) : acc, '') || ''}}",
"options": {}
},
"type": "n8n-nodes-base.jwt",
"typeVersion": 1,
"position": [
300,
5180
],
"id": "ee1351f5-9526-416e-a6f4-0098754ed0c8",
"name": "JWT from current Session",
"alwaysOutputData": false,
"notesInFlow": true,
"credentials": {
"jwtAuth": {
"id": "hhCF430EVcJSsqJs",
"name": "JWT Auth account"
}
},
"onError": "continueErrorOutput",
"notes": "decode and get user creds"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "8e179d53-fa76-410a-b09c-a7c60e2bb7cf",
"name": "=body.email",
"value": "[email protected]",
"type": "string"
},
{
"id": "708c2634-3766-43bf-9009-4a639d46a6d6",
"name": "=body.password",
"value": "abc123",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-40,
5220
],
"id": "de1b1406-8d54-4584-9aaf-7417cbb56b77",
"name": "Fake User1 login info"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "8e179d53-fa76-410a-b09c-a7c60e2bb7cf",
"name": "=body.email",
"value": "[email protected]",
"type": "string"
},
{
"id": "708c2634-3766-43bf-9009-4a639d46a6d6",
"name": "=body.password",
"value": "Bcd123",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-40,
5380
],
"id": "f50b6e2c-a58b-4b8f-8ab8-130f447fee05",
"name": "Fake User2 login info"
},
{
"parameters": {
"content": "โฌ…๏ธ **Note:** In case you add data to the session payload in another node, this node must take its data from that node",
"height": 80,
"width": 300
},
"type": "n8n-nodes-base.stickyNote",
"position": [
1420,
5960
],
"typeVersion": 1,
"id": "017ca515-e06b-4beb-b3e8-f70dac4d2a80",
"name": "Sticky Note40"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6a2a19fc-4043-483b-a642-7aeb774e667b",
"name": "dummy_data",
"value": "=test test test",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
5880
],
"id": "816f89e8-cee1-461d-a010-93395c297664",
"name": "test secondary output",
"retryOnFail": false,
"notesInFlow": true,
"notes": "Just a placeholder for any other output creating node"
},
{
"parameters": {
"promptType": "define",
"text": "={{ $('๐ŸŒ/chat/endpoint.json').item.json.body.chatInput }}",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.9,
"position": [
580,
5720
],
"id": "001f10a9-8e01-4f42-9451-53ef9530531f",
"name": "AI chatting with history",
"onError": "continueRegularOutput"
},
{
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "={{ $('Make payload have a `sessionId`').item.json.payload.sessionId }}",
"contextWindowLength": 10
},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [
680,
5880
],
"id": "5b554ebe-1bbc-4b17-8bfe-953b885ff364",
"name": "Simple Memory (preserve history)",
"notesInFlow": true,
"notes": "(preserve context by SessionID)"
},
{
"parameters": {
"modelName": "={{ $('๐ŸŒ/chat/endpoint.json').item.json.body.model }}",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
520,
5880
],
"id": "e570bf45-41ce-470e-8c19-51cce2148b17",
"name": "Google Gemini Chat Model (by param)",
"credentials": {
"googlePalmApi": {
"id": "WxnPchZ2NK962hUX",
"name": "Google Gemini(PaLM) Api account"
}
}
},
{
"parameters": {
"html": "<template>\n <VApp>\n <VAppBar app> <!-- 'app' makes it fixed at the top -->\n <VAppBarTitle style=\"line-height: .8\">n8n Chat\n <small>powered by <a href=\"https://www.npmjs.com/package/vue3-esm-browser-vueengine\" target=\"_blank\">vueEngine</a></small>\n <br>\n <small style=\"color: gray; font-size: .5em;\">made by <a href=\"https://github.com/BananaAcid\" target=\"_blank\">Nabil Redmann</a></small>\n </VAppBarTitle>\n\n <VSpacer></VSpacer> <!-- Pushes subsequent items to the right -->\n\n <v-select\n ref=\"urlModeSelect\"\n label=\"Use workflow\"\n :items=\"[{title: 'production', value: ''}, {title: 'test', value: '-test'}]\"\n variant=\"solo\"\n :flat=\"true\"\n density=\"compact\"\n v-model=\"urlMode.data\"\n :maxWidth=\"150\"\n ></v-select>\n\n <div v-if=\"loggedInUser?.data\" class=\"d-flex align-center\">\n <VMenu>\n <template v-slot:activator=\"{ props }\">\n <div class=\"d-flex align-center mr-4\" v-bind=\"props\" style=\"cursor: pointer;\">\n <VIcon icon=\"mdi-account-circle\" class=\"mr-1\"></VIcon>\n <span v-html=\"loggedInUser?.data?.username ?? 'no user'\"></span>\n </template>\n\n <VCard>\n <VList>\n <VListItem @click=\"doLogout\">\n <VListItemTitle>Logout</VListItemTitle>\n </VListItem>\n\n <VListItem>\n <VListItemTitle>Debug User Data</VListItemTitle>\n <VListItemSubtitle>\n <pre v-html=\"JSON.stringify(loggedInUser?.data, null, 2)\"></pre>\n </VListItemSubtitle>\n </VListItem>\n </VList>\n </VCard>\n </VMenu>\n\n </div>\n\n </VAppBar>\n\n <VMain>\n <VContainer fluid>\n <UserLogin />\n <RouterView v-if=\"loggedInUser?.data\" />\n </VContainer>\n </VMain>\n </VApp>\n</template>\n\n\n<script setup>\n import { ref, reactive } from 'vue';\n\n let loggedInUser = globalThis.loggedInUser || ( globalThis.loggedInUser = reactive({ data: null }));\n let loadingUser = globalThis.loadingUser || ( globalThis.loadingUser = reactive({ data: false }));\n let urlMode = globalThis.urlMode || ( globalThis.urlMode = reactive({ data: '' }));\n \n let showLogin = ref(false);\n\n // set to test if detected\n if (globalThis.N8N_DATA.webhookUrl.includes('/webhook-test/'))\n urlMode.data = '-test';\n let urlModeSelect = ref();\n if (!globalThis.N8N_DATA.webhookUrl.includes('/webhook/') && !globalThis.N8N_DATA.webhookUrl.includes('/webhook-test/'))\n urlModeSelect.enabled = false;\n\n function doLogout() {\n removeCookie('SESSID_VueEngine_token', '/', document.location.hostname);\n\n loggedInUser.data = null;\n }\n\n function doLogin() {\n showLogin.value = !showLogin.value;\n }\n \n // Mozilla https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support\n function removeCookie(sKey, sPath, sDomain) {\n document.cookie = encodeURIComponent(sKey) + \n \"=; expires=Thu, 01 Jan 1970 00:00:00 GMT\" + \n (sDomain ? \"; domain=\" + sDomain : \"\") + \n (sPath ? \"; path=\" + sPath : \"\");\n }\n</script>\n\n<style lang=\"less\">\n html, body {\n margin: 0;\n padding: 0 !important;\n min-height: 100vh;\n place-items: center;\n }\n #app {\n width: 80em;\n max-width: 80vw;\n }\n\n pre {\n max-height: 15em;\n overflow-y: auto;\n word-wrap:\n break-word;\n }\n\n // remove unused validation message areas\n .v-input__details:has(.v-messages:empty) {\n display: none;\n }\n</style>\n \n<style lang=\"less\">\n .loader {\n animation: pulse 1.5s ease-in-out infinite;\n }\n\n @keyframes pulse {\n 0% {\n opacity: 0.4; /* Startet etwas weniger sichtbar */\n transform: scale(0.95); /* Startet etwas kleiner */\n }\n 50% {\n opacity: 1; /* Wird voll sichtbar */\n transform: scale(1); /* Erreicht normale GrรถรŸe */\n }\n 100% {\n opacity: 0.4; /* Geht zurรผck zu etwas weniger sichtbar */\n transform: scale(0.95); /* Geht zurรผck zu etwas kleiner */\n }\n }\n</style>"
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
400,
4120
],
"id": "25355eb3-cba6-4fd0-863d-e0015db23ded",
"name": "HTML: /App.vue [main]"
},
{
"parameters": {
"html": "{{ /** see https://www.npmjs.com/package/vue3-esm-browser-vueengine **/ '' }}\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>n8n Chat</title>\n <style>\n html {\n min-height: 100vh;\n }\n body {\n font-family: Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background-color: #f0f0f0;\n }\n #initloading {\n position: absolute; /* vuetify overwrites html placeitem in step7, we need to do it this way */\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n line-height: 1;\n }\n #initloading div {\n position: absolute; /* let it behave like a tool tip */\n white-space: nowrap;\n font-size: small;\n color: gray;\n }\n xmp {\n display: none;\n }\n </style>\n <script type=\"module\">globalThis.N8N_DATA = {{ JSON.stringify( $input.all()[0].json ) }};</script>\n </head>\n <body>\n <div id=\"app\">\n <div id=\"initloading\">\n <progress>loading ...</progress>\n <div>Preparing and loading dependencies</div>\n </div>\n </div>\n\n {{\n /** use all items (except the first) that are pages/components/views/app and create the required <xmp> blocks **/\n $input.all().slice(1).map(item => `<xmp type=\"${item.json.type}\" id=\"${item.json.id}\">${item.json.html}</xmp>` ).join('\\n\\n')\n }}\n\n\n <script type=\"importmap\" id=\"vueAdditions\">\n {\n \"imports\": {\n \"@vueuse/shared\": \"https://cdn.jsdelivr.net/npm/@vueuse/shared@13/index.min.mjs\",\n \"@vueuse/core\": \"https://cdn.jsdelivr.net/npm/@vueuse/core@13/index.min.mjs\",\n \"marked\": \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\"\n }\n }\n </script>\n\n <script type=\"module\" async>\n globalThis.vueConfig = globalThis.vueConfig ?? {};\n\n globalThis.vueConfig.routes = [\n { path: '/', page: 'Home.vue' },\n // ...\n ];\n \n globalThis.vueConfig.imports = [\n {\n type: 'style',\n content: '@import url(\"https://unpkg.com/[email protected]/dist/vuetify.min.css\"); ',\n },\n {\n type: 'style',\n content: '@import url(\"https://unpkg.com/@mdi/font@latest/css/materialdesignicons.min.css\"); ',\n },\n {\n type: 'plugin',\n content: async ({ app }) =>\n app.use( (await import('https://unpkg.com/[email protected]/dist/vuetify.esm.js')).createVuetify() ),\n },\n ];\n\n // preloader text\n globalThis.loadingStates = new Proxy([], {\n set: (target, prop, changes) => {\n target[prop] = changes;\n \n // handle status update\n const elS = document.querySelector('#initloading div');\n if (prop !== 'length' && elS) elS.innerHTML += '<br>' + changes;\n if (prop == 'length' && elS) {\n const elL = document.querySelector('#initloading progress');\n elL.value = changes;\n // we set max after the first change, before that, the progress bar will \"bounce\"\n elL.max = 11;\n }\n \n return true;\n },\n });\n </script>\n\n <!-- vueEngine stuff below, no need to modify ------------------------------------------------------->\n\n <script type=\"importmap\" id=\"vueBasics\">\n {\n \"imports\": {\n \"vue\": \"https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js\",\n \"vue-router\": \"https://unpkg.com/vue-router@4/dist/vue-router.esm-browser.prod.js\",\n \"vue3-sfc-loader\": \"https://unpkg.com/[email protected]/dist/vue3-sfc-loader.esm.js\",\n \"less\": \"https://cdn.jsdelivr.net/npm/less@4/dist/less.min.js/+esm\"\n }\n }\n </script>\n\n <script type=\"module\" id=\"vueEngine\" src=\"https://unpkg.com/vue3-esm-browser-vueengine\"></script>\n </body>\n</html>"
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
1240,
4060
],
"id": "539fa79c-74e4-4b55-bbd3-adbcfa4270d8",
"name": "HTML: / [index]",
"alwaysOutputData": true,
"executeOnce": true,
"notesInFlow": true,
"notes": "The base & config for the app"
},
{
"parameters": {
"content": "## โฌ‡๏ธ Start here",
"height": 200,
"width": 200
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-340,
3980
],
"typeVersion": 1,
"id": "e2cdcb45-1ea3-4311-8228-6ebf921b055a",
"name": "Sticky Note41"
},
{
"parameters": {
"numberInputs": 5
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.1,
"position": [
860,
4240
],
"id": "61b5aaf1-8d8b-4460-9779-a0289fb83a3a",
"name": "Combine vue files"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.2,
"position": [
80,
4640
],
"id": "acd5bffa-47cd-4561-8284-523995f94688",
"name": "Respond with models"
},
{
"parameters": {
"url": "=https://generativelanguage.googleapis.com/v1beta/openai/models",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-100,
4640
],
"id": "98837aeb-9af4-4b0d-9f0b-0b9ed75b064e",
"name": "Connect and get models",
"credentials": {
"googlePalmApi": {
"id": "WxnPchZ2NK962hUX",
"name": "Google Gemini(PaLM) Api account"
},
"httpBearerAuth": {
"id": "MYE7BRQCCWIBK7J5",
"name": "Bearer Auth account"
},
"httpHeaderAuth": {
"id": "opzNey3IPOuTOoMF",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"httpMethod": "POST",
"path": "0338f2da-389b-49a4-ad0d-18e02e1f300d/chat/data.json",
"options": {
"responseData": "={\"success\": true, \"now\": \"{{$now}}\"}"
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-300,
4460
],
"id": "136953c5-6739-463f-9b98-4bf7f59da654",
"name": "๐ŸŒ/chat/data.json - JSON dummy data",
"webhookId": "3fa53bdc-7816-4826-a9ae-4a547ef5b7fa"
}
],
"connections": {
"Add 'type=app', 'id=App.vue'2": {
"main": [
[
{
"node": "Combine vue files",
"type": "main",
"index": 0
}
]
]
},
"Combine server data and vue files2": {
"main": [
[
{
"node": "HTML: / [index]",
"type": "main",
"index": 0
}
]
]
},
"FakeDB": {
"main": [
[
{
"node": "Auth Merge",
"type": "main",
"index": 1
}
]
]
},
"FakeDB-Authentication": {
"main": [
[
{
"node": "JWT",
"type": "main",
"index": 0
}
]
]
},
"JWT": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Respond with user data",
"type": "main",
"index": 0
}
]
]
},
"JWT from Session": {
"main": [
[
{
"node": "If isLoggedIn",
"type": "main",
"index": 0
}
],
[
{
"node": "Error message",
"type": "main",
"index": 0
}
]
]
},
"Auth Merge": {
"main": [
[
{
"node": "FakeDB-Authentication",
"type": "main",
"index": 0
}
]
]
},
"JWT token update": {
"main": [
[
{
"node": "Make output error save",
"type": "main",
"index": 1
}
]
]
},
"Add `method=GET`": {
"main": [
[
{
"node": "Combine to work for GET & POST",
"type": "main",
"index": 0
}
]
]
},
"Add `method=POST`": {
"main": [
[
{
"node": "Combine to work for GET & POST",
"type": "main",
"index": 0
}
]
]
},
"Error message": {
"main": [
[
{
"node": "Respond with outputs",
"type": "main",
"index": 0
}
]
]
},
"Make output error save": {
"main": [
[
{
"node": "Respond with outputs",
"type": "main",
"index": 0
}
]
]
},
"Activate Nodes": {
"main": [
[
{
"node": "Auth Merge",
"type": "main",
"index": 0
},
{
"node": "FakeDB",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒ/chat/models.json": {
"main": [
[
{
"node": "Connect and get models",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒ/chat/auth": {
"main": [
[
{
"node": "If body: user & password",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒGET /chat/": {
"main": [
[
{
"node": "Add `method=GET`",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒPOST /chat/": {
"main": [
[
{
"node": "Add `method=POST`",
"type": "main",
"index": 0
}
]
]
},
"If isLoggedIn": {
"main": [
[
{
"node": "Make payload have a `sessionId`",
"type": "main",
"index": 0
}
],
[
{
"node": "Error message",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒ/chat/endpoint.json": {
"main": [
[
{
"node": "JWT from Session",
"type": "main",
"index": 0
}
]
]
},
"Output collector": {
"main": [
[
{
"node": "JWT token update",
"type": "main",
"index": 0
},
{
"node": "Make output error save",
"type": "main",
"index": 0
}
]
]
},
"map JWT login creds": {
"main": [
[
{
"node": "Activate Nodes",
"type": "main",
"index": 0
}
]
]
},
"If body: user & password": {
"main": [
[
{
"node": "Activate Nodes",
"type": "main",
"index": 0
}
],
[
{
"node": "JWT from current Session",
"type": "main",
"index": 0
}
]
]
},
"Combine to work for GET & POST": {
"main": [
[
{
"node": "HTML: /App.vue [main]",
"type": "main",
"index": 0
},
{
"node": "HTML: /components/ModelSelection.vue",
"type": "main",
"index": 0
},
{
"node": "HTML: /Home.vue",
"type": "main",
"index": 0
},
{
"node": "Combine server data and vue files2",
"type": "main",
"index": 0
},
{
"node": "HTML: /components/UserLogin.vue",
"type": "main",
"index": 0
}
]
]
},
"HTML: /components/UserLogin.vue": {
"main": [
[
{
"node": "Add 'type=component', 'id=UserLogin.vue'",
"type": "main",
"index": 0
}
]
]
},
"HTML: /Home.vue": {
"main": [
[
{
"node": "Add 'type=page', 'id=Home.vue'",
"type": "main",
"index": 0
}
]
]
},
"Add 'type=page', 'id=Home.vue'": {
"main": [
[
{
"node": "Combine vue files",
"type": "main",
"index": 2
}
]
]
},
"Add 'type=component', 'id=UserLogin.vue'": {
"main": [
[
{
"node": "Combine vue files",
"type": "main",
"index": 1
}
]
]
},
"HTML: /components/ModelSelection.vue": {
"main": [
[
{
"node": "Add 'type=component', 'id=ModelSelection.vue'",
"type": "main",
"index": 0
}
]
]
},
"Add 'type=component', 'id=ModelSelection.vue'": {
"main": [
[
{
"node": "Combine vue files",
"type": "main",
"index": 3
}
]
]
},
"Make payload have a `sessionId`": {
"main": [
[
{
"node": "AI chatting with history",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒ/chat/auth/backdoor2": {
"main": [
[
{
"node": "Fake User1 login info",
"type": "main",
"index": 0
}
]
]
},
"๐ŸŒ/chat/auth/backdoor": {
"main": [
[
{
"node": "If body: user & password",
"type": "main",
"index": 0
}
]
]
},
"JWT from current Session": {
"main": [
[
{
"node": "map JWT login creds",
"type": "main",
"index": 0
}
],
[
{
"node": "Activate Nodes",
"type": "main",
"index": 0
}
]
]
},
"Fake User1 login info": {
"main": [
[
{
"node": "If body: user & password",
"type": "main",
"index": 0
}
]
]
},
"Fake User2 login info": {
"main": [
[]
]
},
"test secondary output": {
"main": [
[
{
"node": "Output collector",
"type": "main",
"index": 1
}
]
]
},
"AI chatting with history": {
"main": [
[
{
"node": "Output collector",
"type": "main",
"index": 0
},
{
"node": "test secondary output",
"type": "main",
"index": 0
}
]
]
},
"Simple Memory (preserve history)": {
"ai_memory": [
[
{
"node": "AI chatting with history",
"type": "ai_memory",
"index": 0
}
]
]
},
"Google Gemini Chat Model (by param)": {
"ai_languageModel": [
[
{
"node": "AI chatting with history",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"HTML: /App.vue [main]": {
"main": [
[
{
"node": "Add 'type=app', 'id=App.vue'2",
"type": "main",
"index": 0
}
]
]
},
"HTML: / [index]": {
"main": [
[
{
"node": "Respond with WebApp",
"type": "main",
"index": 0
}
]
]
},
"Combine vue files": {
"main": [
[
{
"node": "Combine server data and vue files2",
"type": "main",
"index": 1
}
]
]
},
"Connect and get models": {
"main": [
[
{
"node": "Respond with models",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "9581afab26398a75f061401b855816854bd308deae5b21ead05c2b0441020e53"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment