✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
@ -44,6 +44,20 @@ const nextConfig = {
|
||||
experimental: {
|
||||
outputFileTracingRoot: join(__dirname, '../../'),
|
||||
},
|
||||
webpack: (config, { nextRuntime }) => {
|
||||
if (nextRuntime === 'nodejs') return config
|
||||
|
||||
if (nextRuntime === 'edge') {
|
||||
config.resolve.alias['minio'] = false
|
||||
config.resolve.alias['got'] = false
|
||||
return config
|
||||
}
|
||||
// These packages are imports from the integrations definition files that can be ignored for the client.
|
||||
config.resolve.alias['minio'] = false
|
||||
config.resolve.alias['got'] = false
|
||||
config.resolve.alias['openai'] = false
|
||||
return config
|
||||
},
|
||||
headers: async () => {
|
||||
return [
|
||||
{
|
||||
|
@ -38,8 +38,8 @@
|
||||
"@typebot.io/bot-engine": "workspace:*",
|
||||
"@typebot.io/emails": "workspace:*",
|
||||
"@typebot.io/env": "workspace:*",
|
||||
"@typebot.io/nextjs": "workspace:*",
|
||||
"@typebot.io/js": "workspace:*",
|
||||
"@typebot.io/nextjs": "workspace:*",
|
||||
"@udecode/plate-basic-marks": "21.1.5",
|
||||
"@udecode/plate-common": "21.1.5",
|
||||
"@udecode/plate-core": "21.1.5",
|
||||
@ -69,7 +69,7 @@
|
||||
"libphonenumber-js": "1.10.37",
|
||||
"micro": "10.0.1",
|
||||
"micro-cors": "0.1.1",
|
||||
"next": "13.5.4",
|
||||
"next": "14.0.3",
|
||||
"next-auth": "4.22.1",
|
||||
"nextjs-cors": "2.1.2",
|
||||
"nodemailer": "6.9.3",
|
||||
@ -82,6 +82,7 @@
|
||||
"qs": "6.11.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"slate": "0.94.1",
|
||||
"slate-history": "0.93.0",
|
||||
"slate-react": "0.94.2",
|
||||
@ -96,6 +97,9 @@
|
||||
"devDependencies": {
|
||||
"@chakra-ui/styled-system": "2.9.1",
|
||||
"@playwright/test": "1.36.0",
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
"@typebot.io/forge-repository": "workspace:*",
|
||||
"@typebot.io/forge-schemas": "workspace:*",
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/prisma": "workspace:*",
|
||||
"@typebot.io/schemas": "workspace:*",
|
||||
|
@ -1,12 +1,11 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "clp6onbn200011ab379x5gnea",
|
||||
"id": "clpntvmje00031aboan4plzzx",
|
||||
"name": "Audio ChatGPT",
|
||||
"icon": "🔈",
|
||||
"events": [
|
||||
{
|
||||
"id": "ewnfbo0exlu7ihfu2lu2lusm",
|
||||
"outgoingEdgeId": "knz1ln1so0dfyth76qjkjn1p",
|
||||
"outgoingEdgeId": "f2hmh9jelbqb889l6lx5e1u5",
|
||||
"graphCoordinates": { "x": -228.25, "y": -123.31 },
|
||||
"type": "start"
|
||||
}
|
||||
@ -40,27 +39,23 @@
|
||||
"graphCoordinates": { "x": 445.12, "y": -56.2 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "xikptnw1lp1qxdqo10qhmwy1",
|
||||
"type": "OpenAI",
|
||||
"id": "e57nnbkl97h49jaaslxkg3u0",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "wsdxha9db58gk2v9n1j10m7c",
|
||||
"role": "Dialogue",
|
||||
"dialogueVariableId": "vabkycu0qqff5d6ar2ama16pf",
|
||||
"startsBy": "user"
|
||||
"dialogueVariableId": "vabkycu0qqff5d6ar2ama16pf"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "p8ksqi2jhyzid2od3dikv299",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clp6ooc3700031ab30yof27jm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -73,14 +68,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "prsimdxdol42ty2parzgx8am",
|
||||
"type": "OpenAI",
|
||||
"id": "av59rg9zeqtl73o8icnrr2xd",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"credentialsId": "clp6ooc3700031ab30yof27jm",
|
||||
"task": "Create speech",
|
||||
"model": "tts-1",
|
||||
"input": "{{Assistant Message}}",
|
||||
"voice": "alloy",
|
||||
"action": "Create speech",
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"saveUrlInVariableId": "vgr0iwg95npp7pztkmdyn89m1"
|
||||
}
|
||||
},
|
||||
@ -98,14 +92,13 @@
|
||||
"graphCoordinates": { "x": -222.61, "y": -54.39 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "dp5gx25j73fgmcj9582ydik9",
|
||||
"type": "OpenAI",
|
||||
"id": "yuiyeh0czhpymzwuzrm3af5r",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"credentialsId": "clp6ooc3700031ab30yof27jm",
|
||||
"task": "Create speech",
|
||||
"model": "tts-1",
|
||||
"input": "Hi there! How can I help?",
|
||||
"voice": "alloy",
|
||||
"action": "Create speech",
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"saveUrlInVariableId": "vxw4quja426402hvhtm33tsp3"
|
||||
}
|
||||
},
|
||||
@ -149,9 +142,9 @@
|
||||
{
|
||||
"text": "Once it's done, delete this group and connect the "
|
||||
},
|
||||
{ "text": "Start", "bold": true },
|
||||
{ "bold": true, "text": "Start" },
|
||||
{ "text": " event with " },
|
||||
{ "text": "Intro", "bold": true },
|
||||
{ "bold": true, "text": "Intro" },
|
||||
{ "text": " 🚀\n" }
|
||||
]
|
||||
}
|
||||
@ -180,7 +173,7 @@
|
||||
{
|
||||
"from": { "eventId": "ewnfbo0exlu7ihfu2lu2lusm" },
|
||||
"to": { "groupId": "yswu9fml4zflxaqlujb94ir8" },
|
||||
"id": "knz1ln1so0dfyth76qjkjn1p"
|
||||
"id": "f2hmh9jelbqb889l6lx5e1u5"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
@ -193,12 +186,13 @@
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {},
|
||||
"createdAt": "2023-11-20T09:06:40.430Z",
|
||||
"updatedAt": "2023-11-20T09:20:01.662Z",
|
||||
"createdAt": "2023-12-02T09:05:10.874Z",
|
||||
"updatedAt": "2023-12-02T09:08:20.451Z",
|
||||
"icon": "🔈",
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
"customDomain": null,
|
||||
"workspaceId": "freeWorkspace",
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false,
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "clofz4jhf00071a5pjlh8ruwr",
|
||||
"id": "clpntkei400011aboogh27ead",
|
||||
"name": "Basic ChatGPT",
|
||||
"events": [
|
||||
{
|
||||
"id": "ewnfbo0exlu7ihfu2lu2lusm",
|
||||
"outgoingEdgeId": "q25yjqccpjv3i1tclgv1x941",
|
||||
"outgoingEdgeId": "pn7omb9mx5xxc4mzq028fcmq",
|
||||
"graphCoordinates": { "x": -228.25, "y": -123.31 },
|
||||
"type": "start"
|
||||
}
|
||||
@ -18,7 +18,6 @@
|
||||
"blocks": [
|
||||
{
|
||||
"id": "s6eky7dd3md9hto9y4wsuj7h",
|
||||
"groupId": "t3tv4dm3khwmiotjle5jb65g",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
@ -35,7 +34,6 @@
|
||||
},
|
||||
{
|
||||
"id": "nqsu9f13q5j8tt56bcbuto62",
|
||||
"groupId": "t3tv4dm3khwmiotjle5jb65g",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
@ -63,13 +61,11 @@
|
||||
"blocks": [
|
||||
{
|
||||
"id": "ovgk70u0kfxrbtz9dy4e040o",
|
||||
"groupId": "qfrz5nwm63g12dajsjxothb5",
|
||||
"type": "text input",
|
||||
"options": { "variableId": "vudksu3zyrat6s1bq6qne0rx3" }
|
||||
},
|
||||
{
|
||||
"id": "m4jadtknjb3za3gvxj1xdn1k",
|
||||
"groupId": "qfrz5nwm63g12dajsjxothb5",
|
||||
"outgoingEdgeId": "fpj0xacppqd1s5slyljzhzc9",
|
||||
"type": "Set variable",
|
||||
"options": {
|
||||
@ -86,33 +82,27 @@
|
||||
"graphCoordinates": { "x": 624.57, "y": 200.09 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "xikptnw1lp1qxdqo10qhmwy1",
|
||||
"groupId": "a6ymhjwtkqwp8t127plz8qmk",
|
||||
"type": "OpenAI",
|
||||
"id": "p4q3wbk4wcw818qocrvu7dxs",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "wsdxha9db58gk2v9n1j10m7c",
|
||||
"role": "Dialogue",
|
||||
"dialogueVariableId": "vabkycu0qqff5d6ar2ama16pf",
|
||||
"startsBy": "user"
|
||||
"dialogueVariableId": "vabkycu0qqff5d6ar2ama16pf"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "p8ksqi2jhyzid2od3dikv299",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clocxtxlc00031an2uc59hdpb"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "yblc864bzipaqfja7b2o3oo0",
|
||||
"groupId": "a6ymhjwtkqwp8t127plz8qmk",
|
||||
"type": "Set variable",
|
||||
"options": {
|
||||
"variableId": "vabkycu0qqff5d6ar2ama16pf",
|
||||
@ -122,7 +112,6 @@
|
||||
},
|
||||
{
|
||||
"id": "myldn1l1nfdwwm8qvza71rwv",
|
||||
"groupId": "a6ymhjwtkqwp8t127plz8qmk",
|
||||
"outgoingEdgeId": "y8ml9ljnsydol9b42fd9zdve",
|
||||
"type": "text",
|
||||
"content": {
|
||||
@ -140,7 +129,6 @@
|
||||
"blocks": [
|
||||
{
|
||||
"id": "vzcrfk4vl9gy8igu0ysja5nc",
|
||||
"groupId": "c5f00f3oclwi1srcz10jjt9u",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
@ -150,7 +138,6 @@
|
||||
},
|
||||
{
|
||||
"id": "gphm5wy1md9cunwkdtbzg6nq",
|
||||
"groupId": "c5f00f3oclwi1srcz10jjt9u",
|
||||
"outgoingEdgeId": "h5sk58j0ryrxmfv4gmw7r4dw",
|
||||
"type": "text",
|
||||
"content": {
|
||||
@ -165,32 +152,23 @@
|
||||
"edges": [
|
||||
{
|
||||
"id": "h5sk58j0ryrxmfv4gmw7r4dw",
|
||||
"from": {
|
||||
"groupId": "c5f00f3oclwi1srcz10jjt9u",
|
||||
"blockId": "gphm5wy1md9cunwkdtbzg6nq"
|
||||
},
|
||||
"from": { "blockId": "gphm5wy1md9cunwkdtbzg6nq" },
|
||||
"to": { "groupId": "qfrz5nwm63g12dajsjxothb5" }
|
||||
},
|
||||
{
|
||||
"id": "y8ml9ljnsydol9b42fd9zdve",
|
||||
"from": {
|
||||
"groupId": "a6ymhjwtkqwp8t127plz8qmk",
|
||||
"blockId": "myldn1l1nfdwwm8qvza71rwv"
|
||||
},
|
||||
"from": { "blockId": "myldn1l1nfdwwm8qvza71rwv" },
|
||||
"to": { "groupId": "qfrz5nwm63g12dajsjxothb5" }
|
||||
},
|
||||
{
|
||||
"id": "fpj0xacppqd1s5slyljzhzc9",
|
||||
"from": {
|
||||
"groupId": "qfrz5nwm63g12dajsjxothb5",
|
||||
"blockId": "m4jadtknjb3za3gvxj1xdn1k"
|
||||
},
|
||||
"from": { "blockId": "m4jadtknjb3za3gvxj1xdn1k" },
|
||||
"to": { "groupId": "a6ymhjwtkqwp8t127plz8qmk" }
|
||||
},
|
||||
{
|
||||
"from": { "eventId": "ewnfbo0exlu7ihfu2lu2lusm" },
|
||||
"to": { "groupId": "t3tv4dm3khwmiotjle5jb65g" },
|
||||
"id": "q25yjqccpjv3i1tclgv1x941"
|
||||
"id": "pn7omb9mx5xxc4mzq028fcmq"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
@ -201,8 +179,8 @@
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": { "general": {} },
|
||||
"createdAt": "2023-11-01T16:30:13.155Z",
|
||||
"updatedAt": "2023-11-01T16:30:13.155Z",
|
||||
"createdAt": "2023-12-02T08:56:27.244Z",
|
||||
"updatedAt": "2023-12-02T09:00:25.221Z",
|
||||
"icon": "🤖",
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "cloi9k6tf00051aqji6vk88pq",
|
||||
"id": "clpnu8xj800071abop3o19y02",
|
||||
"name": "ChatGPT personas",
|
||||
"events": [
|
||||
{
|
||||
"id": "w99qhdr20tw02sfrfwkfc1tg",
|
||||
"outgoingEdgeId": "c3733n7ia1hxcwld9lm3p351",
|
||||
"outgoingEdgeId": "k5bj58emklqfqv3hemko4u23",
|
||||
"graphCoordinates": { "x": -95.29, "y": -267.02 },
|
||||
"type": "start"
|
||||
}
|
||||
@ -60,27 +60,24 @@
|
||||
"graphCoordinates": { "x": 1053.297810684862, "y": 919.9658659364646 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "xikptnw1lp1qxdqo10qhmwy1",
|
||||
"type": "OpenAI",
|
||||
"id": "qqlv6ikxqh2l7wjibjqk3j93",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "mcc3hr1us468btys3moj20m9",
|
||||
"role": "user",
|
||||
"content": "Starting from now, I want you to explain things with simple words, as if I'm 11 years old."
|
||||
},
|
||||
{
|
||||
"id": "i8i226uylkh84ovtpguaqc83",
|
||||
"role": "Dialogue",
|
||||
"dialogueVariableId": "vu9adij5penetej2xz89htfe6"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "brb5ccisi1tjiqf3ng0asaiq",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
]
|
||||
@ -111,13 +108,11 @@
|
||||
{
|
||||
"id": "x18iwzwmbzi9jjpnwij1861i",
|
||||
"outgoingEdgeId": "mxl8lftsj3pbmj4g24ymxajo",
|
||||
"type": "button",
|
||||
"content": "Continue"
|
||||
},
|
||||
{
|
||||
"id": "imx7otsonvm0takr02b4ulyo",
|
||||
"outgoingEdgeId": "ny44r5sp69gne7obgshidhph",
|
||||
"type": "button",
|
||||
"content": "Menu"
|
||||
}
|
||||
]
|
||||
@ -127,30 +122,27 @@
|
||||
{
|
||||
"id": "fj5z2nx488htv0843kq6qeyk",
|
||||
"title": "Professor AI reply",
|
||||
"graphCoordinates": { "x": 1040.86, "y": -128.46 },
|
||||
"graphCoordinates": { "x": 1052.26, "y": -56.02 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "f2r11ibqq2ufrahfcl3gf6qi",
|
||||
"type": "OpenAI",
|
||||
"id": "itiwmw62ml38rmeawxxawkub",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "fxg16pnlnwuhfpz1r51xslbd",
|
||||
"role": "user",
|
||||
"content": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations."
|
||||
},
|
||||
{
|
||||
"id": "biqljpsbqfkgno4m80s4j5p0",
|
||||
"role": "Dialogue",
|
||||
"dialogueVariableId": "vu9adij5penetej2xz89htfe6"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "brb5ccisi1tjiqf3ng0asaiq",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
]
|
||||
@ -181,13 +173,11 @@
|
||||
{
|
||||
"id": "zaylo8bstqx0wp6bpdbd1rak",
|
||||
"outgoingEdgeId": "q6o0cbyzxtvgls3jtz7rpdgw",
|
||||
"type": "button",
|
||||
"content": "Continue"
|
||||
},
|
||||
{
|
||||
"id": "d5jv3sjpzobsrnhcp055mxkv",
|
||||
"outgoingEdgeId": "xjv7pkpgpwh169448t8pepg4",
|
||||
"type": "button",
|
||||
"content": "Back to menu"
|
||||
}
|
||||
]
|
||||
@ -197,30 +187,27 @@
|
||||
{
|
||||
"id": "csbysu8dr08zxr4i6hzvzjdf",
|
||||
"title": "Copywriter AI reply",
|
||||
"graphCoordinates": { "x": 1044.25, "y": 372.87 },
|
||||
"graphCoordinates": { "x": 1055.63, "y": 436.9 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "h2t5vbir3zh8eku55ozwb1du",
|
||||
"type": "OpenAI",
|
||||
"id": "w2y6tl8mggplu8vsc9hu2080",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-3.5-turbo",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "fxg16pnlnwuhfpz1r51xslbd",
|
||||
"role": "user",
|
||||
"content": "I want you to act as a copywriter. You will come up with copywriting advices that are engaging, imaginative, and captivating for the audience."
|
||||
},
|
||||
{
|
||||
"id": "ynbhlcbsmy24pobiay9zezli",
|
||||
"role": "Dialogue",
|
||||
"dialogueVariableId": "vu9adij5penetej2xz89htfe6"
|
||||
}
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "brb5ccisi1tjiqf3ng0asaiq",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
]
|
||||
@ -251,13 +238,11 @@
|
||||
{
|
||||
"id": "b6zif4xxe2cuiddc2oqayaxi",
|
||||
"outgoingEdgeId": "jwydpoxngp2gvwanaruphe6s",
|
||||
"type": "button",
|
||||
"content": "Continue"
|
||||
},
|
||||
{
|
||||
"id": "ooib3mqlfkazta6iol1ocloe",
|
||||
"outgoingEdgeId": "gfrpgowch879p1qaj9jzsh01",
|
||||
"type": "button",
|
||||
"content": "Back to menu"
|
||||
}
|
||||
]
|
||||
@ -296,25 +281,20 @@
|
||||
{
|
||||
"id": "rn0lqz1wvsg9lmc0jcl6ps8j",
|
||||
"outgoingEdgeId": "ry7l8wcaidxw5izm7zoy83kj",
|
||||
"type": "button",
|
||||
"content": "English professor"
|
||||
},
|
||||
{
|
||||
"id": "le84cls9vkmrxquvqw8bhp7h",
|
||||
"outgoingEdgeId": "iy2htkuup0l908fsosg6d2qz",
|
||||
"type": "button",
|
||||
"content": "Copywriter"
|
||||
},
|
||||
{
|
||||
"id": "mx4kgfgena53mxf87piwu1j2",
|
||||
"outgoingEdgeId": "kmex71jzzzekni4louuy3xbf",
|
||||
"type": "button",
|
||||
"content": "Concept explainer"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"variableId": "vs7wwz29yyd21pfl4syeptdgi"
|
||||
}
|
||||
"options": { "variableId": "vs7wwz29yyd21pfl4syeptdgi" }
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -700,7 +680,7 @@
|
||||
{
|
||||
"from": { "eventId": "w99qhdr20tw02sfrfwkfc1tg" },
|
||||
"to": { "groupId": "bofjp88arodr4k0btv2esyqy" },
|
||||
"id": "c3733n7ia1hxcwld9lm3p351"
|
||||
"id": "k5bj58emklqfqv3hemko4u23"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
@ -711,8 +691,8 @@
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {},
|
||||
"createdAt": "2023-11-03T06:57:51.747Z",
|
||||
"updatedAt": "2023-11-03T07:03:19.089Z",
|
||||
"createdAt": "2023-12-02T09:15:31.652Z",
|
||||
"updatedAt": "2023-12-02T09:18:34.891Z",
|
||||
"icon": "🎭",
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "clp6pe8dy00051ab3n6coxt62",
|
||||
"id": "clpnu4plq00051abo2487q86h",
|
||||
"name": "ChatGPT condition",
|
||||
"events": [
|
||||
{
|
||||
"id": "ewnfbo0exlu7ihfu2lu2lusm",
|
||||
"outgoingEdgeId": "gj1gs8hdembrsw84aafd1hbj",
|
||||
"outgoingEdgeId": "q2fpvz66ei3gd6k3wwq6w8f2",
|
||||
"graphCoordinates": { "x": -228.25, "y": -123.31 },
|
||||
"type": "start"
|
||||
}
|
||||
@ -39,47 +39,25 @@
|
||||
"graphCoordinates": { "x": 228.67, "y": -50.67 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "wdg7upk4oqp602jqjn06gjf6",
|
||||
"type": "OpenAI",
|
||||
"id": "ufwpq6z392ebsu5tda0md77a",
|
||||
"type": "openai",
|
||||
"options": {
|
||||
"task": "Create chat completion",
|
||||
"model": "gpt-4-1106-preview",
|
||||
"action": "Create chat completion",
|
||||
"messages": [
|
||||
{
|
||||
"id": "s7s7uaurqlmsn3r89c10mk98",
|
||||
"role": "system",
|
||||
"content": "You are helpful assistant doing customer support for a software called Typebot.\n\nIf the user is asking a question about his account, please say \"ACCOUNT\".\n\nIf the user wants to talk to a human, please say \"HUMAN\".\n\nOtherwise, say \"OK\""
|
||||
},
|
||||
{
|
||||
"id": "zrgypmt1wlogakfl06gfxpgk",
|
||||
"role": "user",
|
||||
"content": "Can I talk to a human?"
|
||||
},
|
||||
{
|
||||
"id": "i6ldg74yr9n185oumozb3r6b",
|
||||
"role": "assistant",
|
||||
"content": "HUMAN"
|
||||
},
|
||||
{
|
||||
"id": "eoxa3dxtw8wjdyv9efnxryxk",
|
||||
"role": "user",
|
||||
"content": "I need to check my account"
|
||||
},
|
||||
{
|
||||
"id": "nb7sy9x7g07w5s1sxb83v295",
|
||||
"role": "assistant",
|
||||
"content": "ACCOUNT"
|
||||
},
|
||||
{
|
||||
"id": "zazen7p0cyawtix7der2e923",
|
||||
"role": "user",
|
||||
"content": "{{User Message}}"
|
||||
}
|
||||
{ "role": "user", "content": "Can I talk to a human?" },
|
||||
{ "role": "assistant", "content": "HUMAN" },
|
||||
{ "role": "user", "content": "I need to check my account" },
|
||||
{ "role": "assistant", "content": "ACCOUNT" },
|
||||
{ "role": "user", "content": "{{User Message}}" }
|
||||
],
|
||||
"credentialsId": "clpjnjrbt00051aliw6610w1z",
|
||||
"responseMapping": [
|
||||
{
|
||||
"id": "s7s7uaurqlmsn3r89c10mk98",
|
||||
"valueToExtract": "Message content",
|
||||
"item": "Message content",
|
||||
"variableId": "vni6kwbch8zlq92dclgcivzyr"
|
||||
}
|
||||
]
|
||||
@ -215,9 +193,9 @@
|
||||
{
|
||||
"text": "Once it's done, delete this group and connect the "
|
||||
},
|
||||
{ "text": "Start", "bold": true },
|
||||
{ "bold": true, "text": "Start" },
|
||||
{ "text": " event with " },
|
||||
{ "text": "Intro", "bold": true },
|
||||
{ "bold": true, "text": "Intro" },
|
||||
{ "text": " 🚀\n" }
|
||||
]
|
||||
}
|
||||
@ -267,7 +245,7 @@
|
||||
{
|
||||
"from": { "eventId": "ewnfbo0exlu7ihfu2lu2lusm" },
|
||||
"to": { "groupId": "vafybpsjqcbrbbhi8pwl0gic" },
|
||||
"id": "gj1gs8hdembrsw84aafd1hbj"
|
||||
"id": "q2fpvz66ei3gd6k3wwq6w8f2"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
@ -277,12 +255,13 @@
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {},
|
||||
"createdAt": "2023-11-20T09:27:35.926Z",
|
||||
"updatedAt": "2023-11-20T09:27:59.586Z",
|
||||
"createdAt": "2023-12-02T09:12:14.750Z",
|
||||
"updatedAt": "2023-12-02T09:14:20.047Z",
|
||||
"icon": "🧠",
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
"customDomain": null,
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false,
|
||||
|
3
apps/builder/src/assets/styles/md.css
Normal file
3
apps/builder/src/assets/styles/md.css
Normal file
@ -0,0 +1,3 @@
|
||||
.md-link {
|
||||
text-decoration: underline;
|
||||
}
|
@ -2,6 +2,10 @@ import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
chakra,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
@ -10,7 +14,8 @@ import {
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React from 'react'
|
||||
import React, { ReactNode } from 'react'
|
||||
import { MoreInfoTooltip } from './MoreInfoTooltip'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Props<T extends readonly any[]> = {
|
||||
@ -18,6 +23,11 @@ type Props<T extends readonly any[]> = {
|
||||
onItemSelect: (item: T[number]) => void
|
||||
items: T
|
||||
placeholder?: string
|
||||
label?: string
|
||||
isRequired?: boolean
|
||||
direction?: 'row' | 'column'
|
||||
helperText?: ReactNode
|
||||
moreInfoTooltip?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -25,44 +35,67 @@ export const DropdownList = <T extends readonly any[]>({
|
||||
currentItem,
|
||||
onItemSelect,
|
||||
items,
|
||||
placeholder = '',
|
||||
placeholder,
|
||||
label,
|
||||
isRequired,
|
||||
direction = 'column',
|
||||
helperText,
|
||||
moreInfoTooltip,
|
||||
...props
|
||||
}: Props<T> & ButtonProps) => {
|
||||
const handleMenuItemClick = (operator: T[number]) => () => {
|
||||
onItemSelect(operator)
|
||||
}
|
||||
return (
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
colorScheme="gray"
|
||||
justifyContent="space-between"
|
||||
textAlign="left"
|
||||
{...props}
|
||||
>
|
||||
<chakra.span noOfLines={1} display="block">
|
||||
{currentItem ?? placeholder}
|
||||
</chakra.span>
|
||||
</MenuButton>
|
||||
<Portal>
|
||||
<MenuList maxW="500px" zIndex={1500}>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
<FormControl
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
|
||||
{label}{' '}
|
||||
{moreInfoTooltip && (
|
||||
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
colorScheme="gray"
|
||||
justifyContent="space-between"
|
||||
textAlign="left"
|
||||
w="full"
|
||||
{...props}
|
||||
>
|
||||
<chakra.span noOfLines={1} display="block">
|
||||
{currentItem ?? placeholder ?? 'Select an item'}
|
||||
</chakra.span>
|
||||
</MenuButton>
|
||||
<Portal>
|
||||
<MenuList maxW="500px" zIndex={1500}>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
@ -28,9 +28,9 @@ type Props<T> = {
|
||||
addLabel?: string
|
||||
newItemDefaultProps?: Partial<T>
|
||||
hasDefaultItem?: boolean
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
onItemsChange: (items: ItemWithId<T>[]) => void
|
||||
children: (props: TableListItemProps<T>) => JSX.Element
|
||||
}
|
||||
|
||||
export const TableList = <T,>({
|
||||
@ -39,7 +39,7 @@ export const TableList = <T,>({
|
||||
addLabel = 'Add',
|
||||
newItemDefaultProps,
|
||||
hasDefaultItem,
|
||||
Item,
|
||||
children,
|
||||
ComponentBetweenItems,
|
||||
onItemsChange,
|
||||
}: Props<T>) => {
|
||||
@ -107,7 +107,7 @@ export const TableList = <T,>({
|
||||
justifyContent="center"
|
||||
pb="4"
|
||||
>
|
||||
<Item item={item} onItemChange={handleCellChange(itemIndex)} />
|
||||
{children({ item, onItemChange: handleCellChange(itemIndex) })}
|
||||
<Fade
|
||||
in={showDeleteIndex === itemIndex}
|
||||
style={{
|
||||
|
@ -11,9 +11,10 @@ import {
|
||||
FormLabel,
|
||||
Stack,
|
||||
Text,
|
||||
FormHelperText,
|
||||
} from '@chakra-ui/react'
|
||||
import { Variable, VariableString } from '@typebot.io/schemas'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
@ -31,6 +32,7 @@ type Props<HasVariable extends boolean> = {
|
||||
isRequired?: boolean
|
||||
direction?: 'row' | 'column'
|
||||
suffix?: string
|
||||
helperText?: ReactNode
|
||||
onValueChange: (value?: Value<HasVariable>) => void
|
||||
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
|
||||
|
||||
@ -42,8 +44,9 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
label,
|
||||
moreInfoTooltip,
|
||||
isRequired,
|
||||
direction,
|
||||
direction = 'column',
|
||||
suffix,
|
||||
helperText,
|
||||
...props
|
||||
}: Props<HasVariable>) => {
|
||||
const [value, setValue] = useState(defaultValue?.toString() ?? '')
|
||||
@ -87,7 +90,12 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
}
|
||||
|
||||
const Input = (
|
||||
<ChakraNumberInput onChange={handleValueChange} value={value} {...props}>
|
||||
<ChakraNumberInput
|
||||
onChange={handleValueChange}
|
||||
value={value}
|
||||
w="full"
|
||||
{...props}
|
||||
>
|
||||
<NumberInputField placeholder={props.placeholder} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
@ -105,16 +113,16 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel mb="0" mr="0" flexShrink={0}>
|
||||
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
|
||||
{label}{' '}
|
||||
{moreInfoTooltip && (
|
||||
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
<HStack>
|
||||
<HStack w={direction === 'row' ? undefined : 'full'}>
|
||||
{withVariableButton ?? true ? (
|
||||
<HStack spacing="0">
|
||||
<HStack spacing="0" w="full">
|
||||
{Input}
|
||||
<VariablesButton onSelectVariable={handleVariableSelected} />
|
||||
</HStack>
|
||||
@ -123,6 +131,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
)}
|
||||
{suffix ? <Text>{suffix}</Text> : null}
|
||||
</HStack>
|
||||
{helperText ? <FormHelperText mt="0">{helperText}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
) : (
|
||||
Input
|
||||
)}
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
})
|
||||
|
@ -7,9 +7,11 @@ import {
|
||||
HStack,
|
||||
Textarea as ChakraTextarea,
|
||||
TextareaProps,
|
||||
FormHelperText,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
@ -23,7 +25,9 @@ type Props = {
|
||||
withVariableButton?: boolean
|
||||
isRequired?: boolean
|
||||
placeholder?: string
|
||||
helperText?: ReactNode
|
||||
onChange: (value: string) => void
|
||||
direction?: 'row' | 'column'
|
||||
} & Pick<TextareaProps, 'minH'>
|
||||
|
||||
export const Textarea = ({
|
||||
@ -37,6 +41,8 @@ export const Textarea = ({
|
||||
withVariableButton = true,
|
||||
isRequired,
|
||||
minH,
|
||||
helperText,
|
||||
direction = 'column',
|
||||
}: Props) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const [isTouched, setIsTouched] = useState(false)
|
||||
@ -93,14 +99,20 @@ export const Textarea = ({
|
||||
onBlur={updateCarretPosition}
|
||||
onChange={(e) => changeValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
minH={minH}
|
||||
minH={minH ?? '150px'}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<FormControl isRequired={isRequired}>
|
||||
<FormControl
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel>
|
||||
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
|
||||
{label}{' '}
|
||||
{moreInfoTooltip && (
|
||||
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
|
||||
@ -115,6 +127,7 @@ export const Textarea = ({
|
||||
) : (
|
||||
Textarea
|
||||
)}
|
||||
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
@ -13,15 +13,26 @@ import {
|
||||
Portal,
|
||||
Tag,
|
||||
Text,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormHelperText,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
ChangeEvent,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { byId, isDefined, isNotDefined } from '@typebot.io/lib'
|
||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
|
||||
type Props = {
|
||||
initialVariableId: string | undefined
|
||||
@ -29,12 +40,23 @@ type Props = {
|
||||
onSelectVariable: (
|
||||
variable: Pick<Variable, 'id' | 'name'> | undefined
|
||||
) => void
|
||||
} & InputProps
|
||||
label?: string
|
||||
placeholder?: string
|
||||
helperText?: ReactNode
|
||||
moreInfoTooltip?: string
|
||||
direction?: 'row' | 'column'
|
||||
} & Omit<InputProps, 'placeholder'>
|
||||
|
||||
export const VariableSearchInput = ({
|
||||
initialVariableId,
|
||||
onSelectVariable,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
label,
|
||||
helperText,
|
||||
moreInfoTooltip,
|
||||
direction = 'column',
|
||||
isRequired,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
|
||||
@ -168,114 +190,133 @@ export const VariableSearchInput = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
isLazy
|
||||
offset={[0, 2]}
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
data-testid="variables-input"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onFocus={openDropdown}
|
||||
onKeyDown={handleKeyUp}
|
||||
placeholder={inputProps.placeholder ?? 'Select a variable'}
|
||||
autoComplete="off"
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<Portal containerRef={parentModalRef}>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isCreateVariableButtonDisplayed && (
|
||||
<Button
|
||||
ref={createVariableItemRef}
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
bgColor={
|
||||
keyboardFocusIndex === 0 ? focusedItemBgColor : 'transparent'
|
||||
}
|
||||
>
|
||||
Create
|
||||
<Tag colorScheme="orange" ml="1">
|
||||
<Text noOfLines={0} display="block">
|
||||
{inputValue}
|
||||
</Text>
|
||||
</Tag>
|
||||
</Button>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
const indexInList = isCreateVariableButtonDisplayed
|
||||
? idx + 1
|
||||
: idx
|
||||
return (
|
||||
<Button
|
||||
ref={(el) => (itemsRef.current[idx] = el)}
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
bgColor={
|
||||
keyboardFocusIndex === indexInList
|
||||
? focusedItemBgColor
|
||||
: 'transparent'
|
||||
}
|
||||
transition="none"
|
||||
>
|
||||
<Text noOfLines={0} display="block" pr="2">
|
||||
{item.name}
|
||||
</Text>
|
||||
<FormControl
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
|
||||
{label}{' '}
|
||||
{moreInfoTooltip && (
|
||||
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
isLazy
|
||||
offset={[0, 2]}
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
data-testid="variables-input"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onFocus={openDropdown}
|
||||
onKeyDown={handleKeyUp}
|
||||
placeholder={placeholder ?? 'Select a variable'}
|
||||
autoComplete="off"
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<Portal containerRef={parentModalRef}>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isCreateVariableButtonDisplayed && (
|
||||
<Button
|
||||
ref={createVariableItemRef}
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
bgColor={
|
||||
keyboardFocusIndex === 0
|
||||
? focusedItemBgColor
|
||||
: 'transparent'
|
||||
}
|
||||
>
|
||||
Create
|
||||
<Tag colorScheme="orange" ml="1">
|
||||
<Text noOfLines={0} display="block">
|
||||
{inputValue}
|
||||
</Text>
|
||||
</Tag>
|
||||
</Button>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
const indexInList = isCreateVariableButtonDisplayed
|
||||
? idx + 1
|
||||
: idx
|
||||
return (
|
||||
<Button
|
||||
ref={(el) => (itemsRef.current[idx] = el)}
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
bgColor={
|
||||
keyboardFocusIndex === indexInList
|
||||
? focusedItemBgColor
|
||||
: 'transparent'
|
||||
}
|
||||
transition="none"
|
||||
>
|
||||
<Text noOfLines={0} display="block" pr="2">
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria-label="Rename variable"
|
||||
size="xs"
|
||||
onClick={handleRenameVariableClick(item)}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</HStack>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</Flex>
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria-label="Rename variable"
|
||||
size="xs"
|
||||
onClick={handleRenameVariableClick(item)}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</HStack>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</Flex>
|
||||
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
@ -43,13 +43,13 @@ test.describe.parallel('Buttons input block', () => {
|
||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
||||
await page.click('button[aria-label="Close"]')
|
||||
|
||||
await page.click('[data-testid="block2-icon"]')
|
||||
await page.getByTestId('block block2').click({ position: { x: 0, y: 0 } })
|
||||
await page.click('text=Multiple choice?')
|
||||
await page.getByLabel('Button label:').fill('Go')
|
||||
await page.getByPlaceholder('Select a variable').nth(1).click()
|
||||
await page.getByText('var1').click()
|
||||
await expect(page.getByText('Setvar1')).toBeVisible()
|
||||
await page.click('[data-testid="block2-icon"]')
|
||||
await page.getByTestId('block block2').click({ position: { x: 0, y: 0 } })
|
||||
|
||||
await page.locator('text=Item 1').hover()
|
||||
await page.waitForTimeout(1000)
|
||||
@ -83,7 +83,7 @@ test('Variable buttons should work', async ({ page }) => {
|
||||
await expect(page.locator('text=Ok great!')).toBeVisible()
|
||||
await page.click('text="Item 1"')
|
||||
await page.fill('input[value="Item 1"]', '{{Item 2}}')
|
||||
await page.click('[data-testid="block1-icon"]')
|
||||
await page.getByTestId('block block1').click({ position: { x: 0, y: 0 } })
|
||||
await page.click('text=Multiple choice?')
|
||||
await page.click('text="Restart"')
|
||||
await page
|
||||
|
@ -75,7 +75,7 @@ test.describe.parallel('Picture choice input block', () => {
|
||||
page.locator('typebot-standard').getByText('Third image')
|
||||
).toBeVisible()
|
||||
|
||||
await page.getByTestId('block2-icon').click()
|
||||
await page.getByTestId('block block2').click({ position: { x: 0, y: 0 } })
|
||||
await page.getByText('Multiple choice?').click()
|
||||
await page.getByLabel('Submit button label:').fill('Go')
|
||||
await page.getByRole('button', { name: 'Restart' }).click()
|
||||
@ -94,7 +94,7 @@ test.describe.parallel('Picture choice input block', () => {
|
||||
page.locator('typebot-standard').getByText('First image, Second image')
|
||||
).toBeVisible()
|
||||
|
||||
await page.getByTestId('block2-icon').click()
|
||||
await page.getByTestId('block block2').click({ position: { x: 0, y: 0 } })
|
||||
await page.getByText('Is searchable?').click()
|
||||
await page.getByLabel('Input placeholder:').fill('Search...')
|
||||
await page.getByRole('button', { name: 'Restart' }).click()
|
||||
|
@ -26,7 +26,7 @@ import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
||||
import { CellWithValueStack } from './CellWithValueStack'
|
||||
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
||||
import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
|
||||
import { TableListItemProps, TableList } from '@/components/TableList'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
|
||||
import { RowsFilterTableList } from './RowsFilterTableList'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
@ -177,33 +177,22 @@ const ActionOptions = ({
|
||||
totalRowsToExtract,
|
||||
} as GoogleSheetsBlock['options'])
|
||||
|
||||
const UpdatingCellItem = useMemo(
|
||||
() =>
|
||||
function Component(props: TableListItemProps<Cell>) {
|
||||
return <CellWithValueStack {...props} columns={sheet?.columns ?? []} />
|
||||
},
|
||||
[sheet?.columns]
|
||||
)
|
||||
|
||||
const ExtractingCellItem = useMemo(
|
||||
() =>
|
||||
function Component(props: TableListItemProps<ExtractingCell>) {
|
||||
return (
|
||||
<CellWithVariableIdStack {...props} columns={sheet?.columns ?? []} />
|
||||
)
|
||||
},
|
||||
[sheet?.columns]
|
||||
)
|
||||
|
||||
switch (options.action) {
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return (
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToInsert}
|
||||
onItemsChange={handleInsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<CellWithValueStack
|
||||
item={item}
|
||||
onItemChange={onItemChange}
|
||||
columns={sheet?.columns ?? []}
|
||||
/>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
||||
case GoogleSheetsAction.UPDATE_ROW:
|
||||
return (
|
||||
@ -236,9 +225,16 @@ const ActionOptions = ({
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToUpsert}
|
||||
onItemsChange={handleUpsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<CellWithValueStack
|
||||
item={item}
|
||||
onItemChange={onItemChange}
|
||||
columns={sheet?.columns ?? []}
|
||||
/>
|
||||
)}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -286,10 +282,17 @@ const ActionOptions = ({
|
||||
<TableList<ExtractingCell>
|
||||
initialItems={options.cellsToExtract}
|
||||
onItemsChange={handleExtractingCellsChange}
|
||||
Item={ExtractingCellItem}
|
||||
addLabel="Add a value"
|
||||
hasDefaultItem
|
||||
/>
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<CellWithVariableIdStack
|
||||
item={item}
|
||||
onItemChange={onItemChange}
|
||||
columns={sheet?.columns ?? []}
|
||||
/>
|
||||
)}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Stack>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { TableList, TableListItemProps } from '@/components/TableList'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import {
|
||||
GoogleSheetsGetOptions,
|
||||
RowsFilterComparison,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import { RowsFilterComparisonItem } from './RowsFilterComparisonItem'
|
||||
import { LogicalOperator } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
|
||||
@ -30,18 +30,10 @@ export const RowsFilterTableList = ({
|
||||
const updateLogicalOperator = (logicalOperator: LogicalOperator) =>
|
||||
filter && onFilterChange({ ...filter, logicalOperator })
|
||||
|
||||
const createRowsFilterComparisonItem = useCallback(
|
||||
(props: TableListItemProps<RowsFilterComparison>) => (
|
||||
<RowsFilterComparisonItem {...props} columns={columns} />
|
||||
),
|
||||
[columns]
|
||||
)
|
||||
|
||||
return (
|
||||
<TableList<RowsFilterComparison>
|
||||
initialItems={filter?.comparisons ?? []}
|
||||
onItemsChange={updateComparisons}
|
||||
Item={createRowsFilterComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList
|
||||
@ -52,6 +44,8 @@ export const RowsFilterTableList = ({
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add filter rule"
|
||||
/>
|
||||
>
|
||||
{(props) => <RowsFilterComparisonItem {...props} columns={columns} />}
|
||||
</TableList>
|
||||
)
|
||||
}
|
||||
|
@ -94,12 +94,13 @@ export const OpenAIChatCompletionSettings = ({
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.messages}
|
||||
Item={ChatCompletionMessageItem}
|
||||
onItemsChange={updateMessages}
|
||||
isOrdered
|
||||
hasDefaultItem
|
||||
addLabel="Add message"
|
||||
/>
|
||||
>
|
||||
{(props) => <ChatCompletionMessageItem {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
@ -131,11 +132,12 @@ export const OpenAIChatCompletionSettings = ({
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.responseMapping}
|
||||
Item={ChatCompletionResponseItem}
|
||||
onItemsChange={updateResponseMapping}
|
||||
newItemDefaultProps={{ valueToExtract: 'Message content' }}
|
||||
hasDefaultItem
|
||||
/>
|
||||
>
|
||||
{(props) => <ChatCompletionResponseItem {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||
import { TableList, TableListItemProps } from '@/components/TableList'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||
@ -14,7 +14,7 @@ import {
|
||||
pixelEventTypes,
|
||||
pixelObjectProperties,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/pixel/constants'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const pixelReferenceUrl =
|
||||
'https://developers.facebook.com/docs/meta-pixel/reference#standard-events'
|
||||
@ -69,14 +69,6 @@ export const PixelSettings = ({ options, onOptionsChange }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const Item = useMemo(
|
||||
() =>
|
||||
function Component(props: TableListItemProps<Item>) {
|
||||
return <ParamItem {...props} eventType={options?.eventType} />
|
||||
},
|
||||
[options?.eventType]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<TextInput
|
||||
@ -123,10 +115,13 @@ export const PixelSettings = ({ options, onOptionsChange }: Props) => {
|
||||
).length > 0) && (
|
||||
<TableList
|
||||
initialItems={options?.params ?? []}
|
||||
Item={Item}
|
||||
onItemsChange={updateParams}
|
||||
addLabel="Add parameter"
|
||||
/>
|
||||
>
|
||||
{(props) => (
|
||||
<ParamItem {...props} eventType={options?.eventType} />
|
||||
)}
|
||||
</TableList>
|
||||
)}
|
||||
</SwitchWithRelatedSettings>
|
||||
</Stack>
|
||||
|
@ -154,9 +154,10 @@ export const WebhookAdvancedConfigForm = ({
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.queryParams}
|
||||
onItemsChange={updateQueryParams}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
/>
|
||||
>
|
||||
{(props) => <QueryParamsInputs {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
@ -168,9 +169,10 @@ export const WebhookAdvancedConfigForm = ({
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.headers}
|
||||
onItemsChange={updateHeaders}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
>
|
||||
{(props) => <HeadersInputs {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
@ -203,9 +205,10 @@ export const WebhookAdvancedConfigForm = ({
|
||||
<TableList<VariableForTest>
|
||||
initialItems={options?.variablesForTest}
|
||||
onItemsChange={updateVariablesForTest}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
>
|
||||
{(props) => <VariableForTestInputs {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -235,9 +238,10 @@ export const WebhookAdvancedConfigForm = ({
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options?.responseVariableMapping}
|
||||
onItemsChange={updateResponseVariableMapping}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
>
|
||||
{(props) => <ResponseMappingInputs {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
@ -171,10 +171,11 @@ export const ZemanticAiSettings = ({
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.responseMapping ?? []}
|
||||
Item={SearchResponseItem}
|
||||
onItemsChange={updateResponseMapping}
|
||||
newItemDefaultProps={{ valueToExtract: 'Summary' }}
|
||||
/>
|
||||
>
|
||||
{(props) => <SearchResponseItem {...props} />}
|
||||
</TableList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
@ -24,7 +24,6 @@ export const ConditionForm = ({ condition, onConditionChange }: Props) => {
|
||||
<TableList<Comparison>
|
||||
initialItems={condition?.comparisons}
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList
|
||||
@ -38,6 +37,8 @@ export const ConditionForm = ({ condition, onConditionChange }: Props) => {
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add a comparison"
|
||||
/>
|
||||
>
|
||||
{(props) => <ComparisonItem {...props} />}
|
||||
</TableList>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Flex, HStack, Tooltip, useColorModeValue } from '@chakra-ui/react'
|
||||
import { useBlockDnd } from '@/features/graph/providers/GraphDndProvider'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { HStack } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { BlockIcon } from './BlockIcon'
|
||||
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
@ -13,6 +12,9 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/const
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
import { BlockCardLayout } from './BlockCardLayout'
|
||||
import { ForgedBlockCard } from '@/features/forge/ForgedBlockCard'
|
||||
|
||||
type Props = {
|
||||
type: BlockV6['type']
|
||||
@ -28,6 +30,14 @@ export const BlockCard = (
|
||||
const { t } = useTranslate()
|
||||
const { workspace } = useWorkspace()
|
||||
|
||||
if (enabledBlocks.includes(props.type as (typeof enabledBlocks)[number])) {
|
||||
return (
|
||||
<ForgedBlockCard
|
||||
type={props.type as (typeof enabledBlocks)[number]}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
)
|
||||
}
|
||||
switch (props.type) {
|
||||
case BubbleBlockType.EMBED:
|
||||
return (
|
||||
@ -111,37 +121,3 @@ export const BlockCard = (
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const BlockCardLayout = ({ type, onMouseDown, tooltip, children }: Props) => {
|
||||
const { draggedBlockType } = useBlockDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMouseDown(draggedBlockType === type)
|
||||
}, [draggedBlockType, type])
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<Flex pos="relative">
|
||||
<HStack
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue('gray.200', 'gray.800')}
|
||||
rounded="lg"
|
||||
flex="1"
|
||||
cursor={'grab'}
|
||||
opacity={isMouseDown ? '0.4' : '1'}
|
||||
onMouseDown={handleMouseDown}
|
||||
bgColor={useColorModeValue('gray.50', 'gray.850')}
|
||||
px="4"
|
||||
py="2"
|
||||
_hover={useColorModeValue({ shadow: 'md' }, { bgColor: 'gray.800' })}
|
||||
transition="box-shadow 200ms, background-color 200ms"
|
||||
>
|
||||
{!isMouseDown ? children : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { useBlockDnd } from '@/features/graph/providers/GraphDndProvider'
|
||||
import { Tooltip, Flex, HStack, useColorModeValue } from '@chakra-ui/react'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
type: BlockV6['type']
|
||||
tooltip?: string
|
||||
isDisabled?: boolean
|
||||
children: React.ReactNode
|
||||
onMouseDown: (e: React.MouseEvent, type: BlockV6['type']) => void
|
||||
}
|
||||
|
||||
export const BlockCardLayout = ({
|
||||
type,
|
||||
onMouseDown,
|
||||
tooltip,
|
||||
children,
|
||||
}: Props) => {
|
||||
const { draggedBlockType } = useBlockDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMouseDown(draggedBlockType === type)
|
||||
}, [draggedBlockType, type])
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<Flex pos="relative">
|
||||
<HStack
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue('gray.200', 'gray.800')}
|
||||
rounded="lg"
|
||||
flex="1"
|
||||
cursor={'grab'}
|
||||
opacity={isMouseDown ? '0.4' : '1'}
|
||||
onMouseDown={handleMouseDown}
|
||||
bgColor={useColorModeValue('gray.50', 'gray.850')}
|
||||
px="4"
|
||||
py="2"
|
||||
_hover={useColorModeValue({ shadow: 'md' }, { bgColor: 'gray.800' })}
|
||||
transition="box-shadow 200ms, background-color 200ms"
|
||||
>
|
||||
{!isMouseDown ? children : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import { IconProps, useColorModeValue } from '@chakra-ui/react'
|
||||
import { useColorModeValue } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { FlagIcon, SendEmailIcon, WebhookIcon } from '@/components/icons'
|
||||
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
|
||||
import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon'
|
||||
import { JumpIcon } from '@/features/blocks/logic/jump/components/JumpIcon'
|
||||
import { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo'
|
||||
import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio/components/AudioBubbleIcon'
|
||||
import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed/components/EmbedBubbleIcon'
|
||||
import { ImageBubbleIcon } from '@/features/blocks/bubbles/image/components/ImageBubbleIcon'
|
||||
@ -39,10 +38,12 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/const
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { Block } from '@typebot.io/schemas'
|
||||
import { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo'
|
||||
import { ForgedBlockIcon } from '@/features/forge/ForgedBlockIcon'
|
||||
|
||||
type BlockIconProps = { type: Block['type'] } & IconProps
|
||||
type BlockIconProps = { type: Block['type']; mt?: string }
|
||||
|
||||
export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
||||
export const BlockIcon = ({ type, mt }: BlockIconProps): JSX.Element => {
|
||||
const blue = useColorModeValue('blue.500', 'blue.300')
|
||||
const orange = useColorModeValue('orange.500', 'orange.300')
|
||||
const purple = useColorModeValue('purple.500', 'purple.300')
|
||||
@ -50,76 +51,78 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
||||
|
||||
switch (type) {
|
||||
case BubbleBlockType.TEXT:
|
||||
return <TextBubbleIcon color={blue} {...props} />
|
||||
return <TextBubbleIcon color={blue} mt={mt} />
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <ImageBubbleIcon color={blue} {...props} />
|
||||
return <ImageBubbleIcon color={blue} mt={mt} />
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <VideoBubbleIcon color={blue} {...props} />
|
||||
return <VideoBubbleIcon color={blue} mt={mt} />
|
||||
case BubbleBlockType.EMBED:
|
||||
return <EmbedBubbleIcon color={blue} {...props} />
|
||||
return <EmbedBubbleIcon color={blue} mt={mt} />
|
||||
case BubbleBlockType.AUDIO:
|
||||
return <AudioBubbleIcon color={blue} {...props} />
|
||||
return <AudioBubbleIcon color={blue} mt={mt} />
|
||||
case InputBlockType.TEXT:
|
||||
return <TextInputIcon color={orange} {...props} />
|
||||
return <TextInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.NUMBER:
|
||||
return <NumberInputIcon color={orange} {...props} />
|
||||
return <NumberInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.EMAIL:
|
||||
return <EmailInputIcon color={orange} {...props} />
|
||||
return <EmailInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.URL:
|
||||
return <UrlInputIcon color={orange} {...props} />
|
||||
return <UrlInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.DATE:
|
||||
return <DateInputIcon color={orange} {...props} />
|
||||
return <DateInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.PHONE:
|
||||
return <PhoneInputIcon color={orange} {...props} />
|
||||
return <PhoneInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.CHOICE:
|
||||
return <ButtonsInputIcon color={orange} {...props} />
|
||||
return <ButtonsInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.PICTURE_CHOICE:
|
||||
return <PictureChoiceIcon color={orange} {...props} />
|
||||
return <PictureChoiceIcon color={orange} mt={mt} />
|
||||
case InputBlockType.PAYMENT:
|
||||
return <PaymentInputIcon color={orange} {...props} />
|
||||
return <PaymentInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.RATING:
|
||||
return <RatingInputIcon color={orange} {...props} />
|
||||
return <RatingInputIcon color={orange} mt={mt} />
|
||||
case InputBlockType.FILE:
|
||||
return <FileInputIcon color={orange} {...props} />
|
||||
return <FileInputIcon color={orange} mt={mt} />
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <SetVariableIcon color={purple} {...props} />
|
||||
return <SetVariableIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.CONDITION:
|
||||
return <ConditionIcon color={purple} {...props} />
|
||||
return <ConditionIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <RedirectIcon color={purple} {...props} />
|
||||
return <RedirectIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.SCRIPT:
|
||||
return <ScriptIcon {...props} />
|
||||
return <ScriptIcon mt={mt} />
|
||||
case LogicBlockType.WAIT:
|
||||
return <WaitIcon color={purple} {...props} />
|
||||
return <WaitIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.JUMP:
|
||||
return <JumpIcon color={purple} {...props} />
|
||||
return <JumpIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <TypebotLinkIcon color={purple} {...props} />
|
||||
return <TypebotLinkIcon color={purple} mt={mt} />
|
||||
case LogicBlockType.AB_TEST:
|
||||
return <AbTestIcon color={purple} {...props} />
|
||||
return <AbTestIcon color={purple} mt={mt} />
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
return <GoogleSheetsLogo mt={mt} />
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
return <GoogleAnalyticsLogo mt={mt} />
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <WebhookIcon {...props} />
|
||||
return <WebhookIcon mt={mt} />
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <ZapierLogo {...props} />
|
||||
return <ZapierLogo mt={mt} />
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <MakeComLogo {...props} />
|
||||
return <MakeComLogo mt={mt} />
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <PabblyConnectLogo {...props} />
|
||||
return <PabblyConnectLogo mt={mt} />
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <SendEmailIcon {...props} />
|
||||
return <SendEmailIcon mt={mt} />
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <ChatwootLogo {...props} />
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <OpenAILogo fill={openAIColor} {...props} />
|
||||
return <ChatwootLogo mt={mt} />
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return <PixelLogo {...props} />
|
||||
return <PixelLogo mt={mt} />
|
||||
case IntegrationBlockType.ZEMANTIC_AI:
|
||||
return <ZemanticAiLogo {...props} />
|
||||
return <ZemanticAiLogo mt={mt} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
return <FlagIcon mt={mt} />
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <OpenAILogo mt={mt} fill={openAIColor} />
|
||||
default:
|
||||
return <ForgedBlockIcon type={type} mt={mt} />
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/const
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { Block } from '@typebot.io/schemas'
|
||||
import { ForgedBlockLabel } from '@/features/forge/ForgedBlockLabel'
|
||||
|
||||
type Props = { type: Block['type'] }
|
||||
|
||||
@ -98,5 +99,7 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
|
||||
return (
|
||||
<Text fontSize="sm">{t('editor.sidebarBlock.zemanticAi.label')}</Text>
|
||||
)
|
||||
default:
|
||||
return <ForgedBlockLabel type={type} />
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
@ -22,6 +23,13 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/const
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
|
||||
// Integration blocks migrated to forged blocks
|
||||
const legacyIntegrationBlocks = [
|
||||
IntegrationBlockType.OPEN_AI,
|
||||
IntegrationBlockType.ZEMANTIC_AI,
|
||||
]
|
||||
|
||||
export const BlocksSideBar = () => {
|
||||
const { t } = useTranslate()
|
||||
@ -160,9 +168,16 @@ export const BlocksSideBar = () => {
|
||||
{t('editor.sidebarBlocks.blockType.integrations.heading')}
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(IntegrationBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
{Object.values(IntegrationBlockType)
|
||||
.concat(enabledBlocks as any)
|
||||
.filter((type) => !legacyIntegrationBlocks.includes(type))
|
||||
.map((type) => (
|
||||
<BlockCard
|
||||
key={type}
|
||||
type={type}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
|
23
apps/builder/src/features/forge/ForgedBlockCard.tsx
Normal file
23
apps/builder/src/features/forge/ForgedBlockCard.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
import { BlockIcon } from '../editor/components/BlockIcon'
|
||||
import { BlockLabel } from '../editor/components/BlockLabel'
|
||||
import { useForgedBlock } from './hooks/useForgedBlock'
|
||||
import { BlockCardLayout } from '../editor/components/BlockCardLayout'
|
||||
|
||||
export const ForgedBlockCard = (props: {
|
||||
type: ForgedBlock['type']
|
||||
onMouseDown: (e: React.MouseEvent, type: BlockV6['type']) => void
|
||||
}) => {
|
||||
const { blockDef } = useForgedBlock(props.type)
|
||||
|
||||
return (
|
||||
<BlockCardLayout
|
||||
{...props}
|
||||
tooltip={blockDef?.fullName ? blockDef.fullName : undefined}
|
||||
>
|
||||
<BlockIcon type={props.type} />
|
||||
<BlockLabel type={props.type} />
|
||||
</BlockCardLayout>
|
||||
)
|
||||
}
|
18
apps/builder/src/features/forge/ForgedBlockIcon.tsx
Normal file
18
apps/builder/src/features/forge/ForgedBlockIcon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useColorMode } from '@chakra-ui/react'
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { useForgedBlock } from './hooks/useForgedBlock'
|
||||
|
||||
export const ForgedBlockIcon = ({
|
||||
type,
|
||||
mt,
|
||||
}: {
|
||||
type: ForgedBlock['type']
|
||||
mt?: string
|
||||
}): JSX.Element => {
|
||||
const { colorMode } = useColorMode()
|
||||
const { blockDef } = useForgedBlock(type)
|
||||
if (!blockDef) return <></>
|
||||
if (colorMode === 'dark' && blockDef.DarkLogo)
|
||||
return <blockDef.DarkLogo width="1rem" style={{ marginTop: mt }} />
|
||||
return <blockDef.LightLogo width="1rem" style={{ marginTop: mt }} />
|
||||
}
|
9
apps/builder/src/features/forge/ForgedBlockLabel.tsx
Normal file
9
apps/builder/src/features/forge/ForgedBlockLabel.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { useForgedBlock } from './hooks/useForgedBlock'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
|
||||
export const ForgedBlockLabel = ({ type }: { type: ForgedBlock['type'] }) => {
|
||||
const { blockDef } = useForgedBlock(type)
|
||||
|
||||
return <Text fontSize="sm">{blockDef?.name}</Text>
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
|
||||
import { z } from 'zod'
|
||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||
import { forgedCredentialsSchemas } from '@typebot.io/forge-schemas'
|
||||
|
||||
const inputShape = {
|
||||
data: true,
|
||||
type: true,
|
||||
workspaceId: true,
|
||||
name: true,
|
||||
} as const
|
||||
|
||||
export const createCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/credentials',
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
credentials: z.discriminatedUnion(
|
||||
'type',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
forgedCredentialsSchemas.map((i) => i.pick(inputShape))
|
||||
),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { credentials }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: credentials.workspaceId,
|
||||
},
|
||||
select: { id: true, members: true },
|
||||
})
|
||||
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
const { encryptedData, iv } = await encrypt(credentials.data)
|
||||
const createdCredentials = await prisma.credentials.create({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
data: {
|
||||
...credentials,
|
||||
data: encryptedData,
|
||||
iv,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
return { credentialsId: createdCredentials.id }
|
||||
})
|
@ -0,0 +1,43 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||
|
||||
export const deleteCredentials = authenticatedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: {
|
||||
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
|
||||
},
|
||||
},
|
||||
select: { id: true, members: true },
|
||||
})
|
||||
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Workspace not found',
|
||||
})
|
||||
|
||||
await prisma.credentials.delete({
|
||||
where: {
|
||||
id: credentialsId,
|
||||
},
|
||||
})
|
||||
return { credentialsId }
|
||||
}
|
||||
)
|
@ -0,0 +1,43 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
|
||||
export const listCredentials = authenticatedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
type: z.enum(enabledBlocks),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentials: z.array(z.object({ id: z.string(), name: z.string() })),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
members: true,
|
||||
credentials: {
|
||||
where: {
|
||||
type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!workspace || isReadWorkspaceFobidden(workspace, user))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
return { credentials: workspace.credentials }
|
||||
})
|
10
apps/builder/src/features/forge/api/credentials/router.ts
Normal file
10
apps/builder/src/features/forge/api/credentials/router.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { createCredentials } from './createCredentials'
|
||||
import { deleteCredentials } from './deleteCredentials'
|
||||
import { listCredentials } from './listCredentials'
|
||||
|
||||
export const forgedCredentialsRouter = router({
|
||||
createCredentials,
|
||||
listCredentials,
|
||||
deleteCredentials,
|
||||
})
|
81
apps/builder/src/features/forge/api/fetchSelectItems.ts
Normal file
81
apps/builder/src/features/forge/api/fetchSelectItems.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||
import { forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
|
||||
export const fetchSelectItems = authenticatedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
integrationId: z.string(),
|
||||
fetcherId: z.string(),
|
||||
options: z.any(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
items: z.array(
|
||||
z.string().or(z.object({ label: z.string(), value: z.string() }))
|
||||
),
|
||||
})
|
||||
)
|
||||
.query(
|
||||
async ({
|
||||
input: { workspaceId, integrationId, fetcherId, options },
|
||||
ctx: { user },
|
||||
}) => {
|
||||
if (!options.credentialsId) return { items: [] }
|
||||
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: { id: workspaceId },
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
where: {
|
||||
id: options.credentialsId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
data: true,
|
||||
iv: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!workspace || isReadWorkspaceFobidden(workspace, user))
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No workspace found',
|
||||
})
|
||||
|
||||
const credentials = workspace.credentials.at(0)
|
||||
|
||||
if (!credentials) return { items: [] }
|
||||
|
||||
const credentialsData = await decrypt(credentials.data, credentials.iv)
|
||||
|
||||
const blockDef = forgedBlocks.find((b) => b.id === integrationId)
|
||||
|
||||
const fetchers = (blockDef?.fetchers ?? []).concat(
|
||||
blockDef?.actions.flatMap((action) => action.fetchers ?? []) ?? []
|
||||
)
|
||||
const fetcher = fetchers.find((fetcher) => fetcher.id === fetcherId)
|
||||
|
||||
if (!fetcher) return { items: [] }
|
||||
|
||||
return {
|
||||
items: await fetcher.fetch({
|
||||
credentials: credentialsData,
|
||||
options,
|
||||
}),
|
||||
}
|
||||
}
|
||||
)
|
6
apps/builder/src/features/forge/api/router.ts
Normal file
6
apps/builder/src/features/forge/api/router.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { fetchSelectItems } from './fetchSelectItems'
|
||||
|
||||
export const integrationsRouter = router({
|
||||
fetchSelectItems,
|
||||
})
|
115
apps/builder/src/features/forge/components/ForgeSelectInput.tsx
Normal file
115
apps/builder/src/features/forge/components/ForgeSelectInput.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
type Props = {
|
||||
blockDef: ForgedBlockDefinition
|
||||
defaultValue?: string
|
||||
fetcherId: string
|
||||
options: ForgedBlock['options']
|
||||
placeholder?: string
|
||||
label?: string
|
||||
helperText?: ReactNode
|
||||
moreInfoTooltip?: string
|
||||
direction?: 'row' | 'column'
|
||||
isRequired?: boolean
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
export const ForgeSelectInput = ({
|
||||
defaultValue,
|
||||
fetcherId,
|
||||
options,
|
||||
blockDef,
|
||||
placeholder,
|
||||
label,
|
||||
helperText,
|
||||
moreInfoTooltip,
|
||||
isRequired,
|
||||
direction = 'column',
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const baseFetcher = useMemo(() => {
|
||||
const fetchers = blockDef.fetchers ?? []
|
||||
return fetchers.find((fetcher) => fetcher.id === fetcherId)
|
||||
}, [blockDef.fetchers, fetcherId])
|
||||
|
||||
const actionFetcher = useMemo(() => {
|
||||
if (baseFetcher) return
|
||||
const fetchers = blockDef.actions.flatMap((action) => action.fetchers ?? [])
|
||||
return fetchers.find((fetcher) => fetcher.id === fetcherId)
|
||||
}, [baseFetcher, blockDef.actions, fetcherId])
|
||||
|
||||
const { data } = trpc.integrations.fetchSelectItems.useQuery(
|
||||
{
|
||||
integrationId: blockDef.id,
|
||||
options: pick(options, [
|
||||
...(actionFetcher ? ['action'] : []),
|
||||
...(blockDef.auth ? ['credentialsId'] : []),
|
||||
...((baseFetcher
|
||||
? baseFetcher.dependencies
|
||||
: actionFetcher?.dependencies) ?? []),
|
||||
]),
|
||||
workspaceId: workspace?.id as string,
|
||||
fetcherId,
|
||||
},
|
||||
{
|
||||
enabled: !!workspace?.id && (!!baseFetcher || !!actionFetcher),
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel mb="0" mr="0" flexShrink={0}>
|
||||
{label}{' '}
|
||||
{moreInfoTooltip && (
|
||||
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Select
|
||||
items={data?.items}
|
||||
selectedItem={defaultValue}
|
||||
onSelect={onChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||
if (!obj) return {} as Pick<T, K>
|
||||
const ret: any = {}
|
||||
keys.forEach((key) => {
|
||||
ret[key] = obj[key]
|
||||
})
|
||||
return ret
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { useForgedBlock } from '../hooks/useForgedBlock'
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
type Props = {
|
||||
block: ForgedBlock
|
||||
}
|
||||
export const ForgedBlockNodeContent = ({ block }: Props) => {
|
||||
const { blockDef, actionDef } = useForgedBlock(
|
||||
block.type,
|
||||
block.options?.action
|
||||
)
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const setVariableIds = actionDef?.getSetVariableIds?.(block.options) ?? []
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text
|
||||
color={block.options?.action ? 'currentcolor' : 'gray.500'}
|
||||
noOfLines={1}
|
||||
>
|
||||
{block.options?.action &&
|
||||
(!blockDef?.auth || block.options.credentialsId)
|
||||
? block.options.action
|
||||
: 'Configure...'}
|
||||
</Text>
|
||||
{typebot &&
|
||||
setVariableIds.map((variableId, idx) => (
|
||||
<SetVariableLabel
|
||||
key={variableId + idx}
|
||||
variables={typebot.variables}
|
||||
variableId={variableId}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { Stack, useDisclosure } from '@chakra-ui/react'
|
||||
import { BlockOptions } from '@typebot.io/schemas'
|
||||
import { ForgedCredentialsDropdown } from './credentials/ForgedCredentialsDropdown'
|
||||
import { ForgedCredentialsModal } from './credentials/ForgedCredentialsModal'
|
||||
import { ZodObjectLayout } from './zodLayouts/ZodObjectLayout'
|
||||
import { ZodActionDiscriminatedUnion } from './zodLayouts/ZodActionDiscriminatedUnion'
|
||||
import { useForgedBlock } from '../hooks/useForgedBlock'
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
type Props = {
|
||||
block: ForgedBlock
|
||||
onOptionsChange: (options: BlockOptions) => void
|
||||
}
|
||||
export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
||||
const { blockDef, blockSchema } = useForgedBlock(block.type)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
const updateCredentialsId = (credentialsId?: string) => {
|
||||
onOptionsChange({
|
||||
...block.options,
|
||||
credentialsId,
|
||||
})
|
||||
}
|
||||
|
||||
if (!blockDef || !blockSchema) return null
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{blockDef.auth && (
|
||||
<>
|
||||
<ForgedCredentialsModal
|
||||
blockDef={blockDef}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onNewCredentials={updateCredentialsId}
|
||||
/>
|
||||
<ForgedCredentialsDropdown
|
||||
key={block.options?.credentialsId ?? 'none'}
|
||||
blockDef={blockDef}
|
||||
currentCredentialsId={block.options?.credentialsId}
|
||||
onCredentialsSelect={updateCredentialsId}
|
||||
onAddClick={onOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(block.options !== undefined || blockDef.auth === undefined) && (
|
||||
<>
|
||||
{blockDef.options && (
|
||||
<ZodObjectLayout
|
||||
schema={blockDef.options}
|
||||
data={block.options}
|
||||
blockOptions={block.options}
|
||||
blockDef={blockDef}
|
||||
onDataChange={onOptionsChange}
|
||||
/>
|
||||
)}
|
||||
<ZodActionDiscriminatedUnion
|
||||
schema={blockSchema.shape.options}
|
||||
blockDef={blockDef}
|
||||
blockOptions={block.options}
|
||||
onDataChange={onOptionsChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-schemas'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
|
||||
type Props = Omit<ButtonProps, 'type'> & {
|
||||
blockDef: ForgedBlockDefinition
|
||||
currentCredentialsId: string | undefined
|
||||
onAddClick: () => void
|
||||
onCredentialsSelect: (credentialId?: string) => void
|
||||
}
|
||||
|
||||
export const ForgedCredentialsDropdown = ({
|
||||
currentCredentialsId,
|
||||
blockDef,
|
||||
onCredentialsSelect,
|
||||
onAddClick,
|
||||
...props
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const { showToast } = useToast()
|
||||
const { workspace, currentRole } = useWorkspace()
|
||||
const { data, refetch, isLoading } =
|
||||
trpc.integrationCredentials.listCredentials.useQuery(
|
||||
{
|
||||
workspaceId: workspace?.id as string,
|
||||
type: blockDef.id,
|
||||
},
|
||||
{ enabled: !!workspace?.id }
|
||||
)
|
||||
const [isDeleting, setIsDeleting] = useState<string>()
|
||||
|
||||
const { mutate } = trpc.credentials.deleteCredentials.useMutation({
|
||||
onMutate: ({ credentialsId }) => {
|
||||
setIsDeleting(credentialsId)
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
description: error.message,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ credentialsId }) => {
|
||||
if (credentialsId === currentCredentialsId) onCredentialsSelect(undefined)
|
||||
refetch()
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsDeleting(undefined)
|
||||
},
|
||||
})
|
||||
|
||||
const currentCredential = data?.credentials.find(
|
||||
(c) => c.id === currentCredentialsId
|
||||
)
|
||||
|
||||
const handleMenuItemClick = useCallback(
|
||||
(credentialsId: string) => () => {
|
||||
onCredentialsSelect(credentialsId)
|
||||
},
|
||||
[onCredentialsSelect]
|
||||
)
|
||||
|
||||
const clearQueryParams = useCallback(() => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
if (router.query.credentialsId) {
|
||||
handleMenuItemClick(router.query.credentialsId.toString())()
|
||||
clearQueryParams()
|
||||
}
|
||||
}, [
|
||||
clearQueryParams,
|
||||
handleMenuItemClick,
|
||||
router.isReady,
|
||||
router.query.credentialsId,
|
||||
])
|
||||
|
||||
const deleteCredentials =
|
||||
(credentialsId: string) => async (e: React.MouseEvent) => {
|
||||
if (!workspace) return
|
||||
e.stopPropagation()
|
||||
mutate({ workspaceId: workspace.id, credentialsId })
|
||||
}
|
||||
|
||||
if (!data || data?.credentials.length === 0) {
|
||||
return (
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
textAlign="left"
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={onAddClick}
|
||||
isDisabled={currentRole === 'GUEST'}
|
||||
isLoading={isLoading}
|
||||
{...props}
|
||||
>
|
||||
Add {blockDef.auth.name}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
colorScheme="gray"
|
||||
justifyContent="space-between"
|
||||
textAlign="left"
|
||||
{...props}
|
||||
>
|
||||
<Text
|
||||
noOfLines={1}
|
||||
overflowY="visible"
|
||||
h={props.size === 'sm' ? '18px' : '20px'}
|
||||
>
|
||||
{currentCredential
|
||||
? currentCredential.name
|
||||
: `Select ${blockDef.auth.name}`}
|
||||
</Text>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{data?.credentials.map((credentials) => (
|
||||
<MenuItem
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={credentials.id}
|
||||
onClick={handleMenuItemClick(credentials.id)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{credentials.name}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove credentials"
|
||||
size="xs"
|
||||
onClick={deleteCredentials(credentials.id)}
|
||||
isLoading={isDeleting === credentials.id}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
{currentRole === 'GUEST' ? null : (
|
||||
<MenuItem
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
icon={<PlusIcon />}
|
||||
onClick={onAddClick}
|
||||
>
|
||||
Connect new
|
||||
</MenuItem>
|
||||
)}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
import { TextInput } from '@/components/inputs/TextInput'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useState } from 'react'
|
||||
import { ZodObjectLayout } from '../zodLayouts/ZodObjectLayout'
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-schemas'
|
||||
|
||||
type Props = {
|
||||
blockDef: ForgedBlockDefinition
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const ForgedCredentialsModal = ({
|
||||
blockDef,
|
||||
isOpen,
|
||||
onClose,
|
||||
onNewCredentials,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const [name, setName] = useState('')
|
||||
const [data, setData] = useState({})
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
|
||||
const {
|
||||
credentials: {
|
||||
listCredentials: { refetch: refetchCredentials },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
const { mutate } = trpc.integrationCredentials.createCredentials.useMutation({
|
||||
onMutate: () => setIsCreating(true),
|
||||
onSettled: () => setIsCreating(false),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
refetchCredentials()
|
||||
onNewCredentials(data.credentialsId)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const createOpenAICredentials = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!workspace) return
|
||||
mutate({
|
||||
credentials: {
|
||||
type: blockDef.id,
|
||||
workspaceId: workspace.id,
|
||||
name,
|
||||
data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!blockDef.auth) return null
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add {blockDef.auth.name}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<form onSubmit={createOpenAICredentials}>
|
||||
<ModalBody as={Stack} spacing="6">
|
||||
<TextInput
|
||||
isRequired
|
||||
label="Name"
|
||||
onChange={setName}
|
||||
placeholder="My account"
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
<ZodObjectLayout
|
||||
schema={blockDef.auth.schema}
|
||||
data={data}
|
||||
onDataChange={setData}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isCreating}
|
||||
isDisabled={Object.keys(data).length === 0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
import { useMemo } from 'react'
|
||||
import { ZodObjectLayout } from './ZodObjectLayout'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
type Props = {
|
||||
blockDef?: ForgedBlockDefinition
|
||||
blockOptions?: ForgedBlock['options']
|
||||
schema: z.ZodOptional<z.ZodDiscriminatedUnion<'action', z.ZodObject<any>[]>>
|
||||
onDataChange: (options: ForgedBlock['options']) => void
|
||||
}
|
||||
|
||||
export const ZodActionDiscriminatedUnion = ({
|
||||
blockDef,
|
||||
blockOptions,
|
||||
schema,
|
||||
onDataChange,
|
||||
}: Props) => {
|
||||
const innerSchema = schema._def.innerType
|
||||
const currentOptions = blockOptions?.action
|
||||
? innerSchema._def.optionsMap.get(blockOptions?.action)
|
||||
: undefined
|
||||
const keysBeforeActionField = useMemo(() => {
|
||||
if (!currentOptions) return []
|
||||
return Object.keys(currentOptions.shape).slice(
|
||||
0,
|
||||
Object.keys(currentOptions.shape).findIndex((key) => key === 'action') + 1
|
||||
)
|
||||
}, [currentOptions])
|
||||
return (
|
||||
<>
|
||||
<DropdownList
|
||||
currentItem={blockOptions?.action}
|
||||
onItemSelect={(item) => onDataChange({ ...blockOptions, action: item })}
|
||||
items={
|
||||
[...innerSchema._def.optionsMap.keys()].filter(isDefined) as string[]
|
||||
}
|
||||
placeholder="Select an action"
|
||||
/>
|
||||
{currentOptions && (
|
||||
<ZodObjectLayout
|
||||
schema={currentOptions}
|
||||
data={blockOptions}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
onDataChange={onDataChange}
|
||||
ignoreKeys={keysBeforeActionField}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
import { ZodObjectLayout } from './ZodObjectLayout'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const ZodDiscriminatedUnionLayout = ({
|
||||
discriminant,
|
||||
data,
|
||||
schema,
|
||||
dropdownPlaceholder,
|
||||
blockDef,
|
||||
blockOptions,
|
||||
onDataChange,
|
||||
}: {
|
||||
discriminant: string
|
||||
data: any
|
||||
schema: z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
|
||||
dropdownPlaceholder: string
|
||||
blockDef?: ForgedBlockDefinition
|
||||
blockOptions?: ForgedBlock['options']
|
||||
onDataChange: (value: string) => void
|
||||
}) => {
|
||||
const currentOptions = data?.[discriminant]
|
||||
? schema._def.optionsMap.get(data?.[discriminant])
|
||||
: undefined
|
||||
return (
|
||||
<>
|
||||
<DropdownList
|
||||
currentItem={data?.[discriminant]}
|
||||
onItemSelect={(item) => onDataChange({ ...data, [discriminant]: item })}
|
||||
items={
|
||||
[...schema._def.optionsMap.keys()].filter(
|
||||
(key) =>
|
||||
isDefined(key) &&
|
||||
!schema._def.optionsMap.get(key)?._def.layout?.isHidden
|
||||
) as string[]
|
||||
}
|
||||
placeholder={dropdownPlaceholder}
|
||||
/>
|
||||
{currentOptions && (
|
||||
<ZodObjectLayout
|
||||
schema={currentOptions}
|
||||
data={data}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
import { NumberInput, TextInput, Textarea } from '@/components/inputs'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
import { ZodLayoutMetadata } from '@typebot.io/forge/zod'
|
||||
import Markdown, { Components } from 'react-markdown'
|
||||
import { ZodTypeAny } from 'zod'
|
||||
import { ForgeSelectInput } from '../ForgeSelectInput'
|
||||
import { ZodObjectLayout } from './ZodObjectLayout'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import { ZodDiscriminatedUnionLayout } from './ZodDiscriminatedUnionLayout'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
const mdComponents = {
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="md-link"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
} satisfies Components
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const ZodFieldLayout = ({
|
||||
data,
|
||||
schema,
|
||||
isInAccordion,
|
||||
blockDef,
|
||||
blockOptions,
|
||||
onDataChange,
|
||||
}: {
|
||||
data: any
|
||||
schema: z.ZodTypeAny
|
||||
isInAccordion?: boolean
|
||||
blockDef?: ForgedBlockDefinition
|
||||
blockOptions?: ForgedBlock['options']
|
||||
onDataChange: (val: any) => void
|
||||
}) => {
|
||||
const layout = schema._def.layout as ZodLayoutMetadata<ZodTypeAny> | undefined
|
||||
const type = schema._def.innerType
|
||||
? schema._def.innerType._def.typeName
|
||||
: schema._def.typeName
|
||||
|
||||
if (layout?.isHidden) return null
|
||||
switch (type) {
|
||||
case 'ZodObject':
|
||||
return (
|
||||
<ZodObjectLayout
|
||||
schema={schema as z.ZodObject<any>}
|
||||
data={data}
|
||||
onDataChange={onDataChange}
|
||||
isInAccordion={isInAccordion}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
/>
|
||||
)
|
||||
case 'ZodDiscriminatedUnion': {
|
||||
return (
|
||||
<ZodDiscriminatedUnionLayout
|
||||
discriminant={schema._def.discriminator}
|
||||
data={data}
|
||||
schema={schema as z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>}
|
||||
dropdownPlaceholder={`Select a ${schema._def.discriminator}`}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'ZodArray': {
|
||||
if (layout?.accordion)
|
||||
return (
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
{layout?.accordion}
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel as={Stack} pt="4">
|
||||
<ZodArrayContent
|
||||
data={data}
|
||||
schema={schema}
|
||||
layout={layout}
|
||||
onDataChange={onDataChange}
|
||||
isInAccordion
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
return (
|
||||
<ZodArrayContent
|
||||
data={data}
|
||||
schema={schema}
|
||||
layout={layout}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'ZodEnum': {
|
||||
return (
|
||||
<DropdownList
|
||||
currentItem={data ?? layout?.defaultValue}
|
||||
onItemSelect={onDataChange}
|
||||
items={schema._def.innerType._def.values}
|
||||
label={layout?.label}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>{layout.helperText}</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
placeholder={layout?.placeholder}
|
||||
direction={layout?.direction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'ZodNumber':
|
||||
case 'ZodUnion': {
|
||||
return (
|
||||
<NumberInput
|
||||
defaultValue={data ?? layout?.defaultValue}
|
||||
label={layout?.label}
|
||||
placeholder={layout?.placeholder}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>{layout.helperText}</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
isRequired={layout?.isRequired}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onValueChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'ZodString': {
|
||||
if (layout?.fetcher) {
|
||||
if (!blockDef) return null
|
||||
return (
|
||||
<ForgeSelectInput
|
||||
defaultValue={data ?? layout.defaultValue}
|
||||
placeholder={layout.placeholder}
|
||||
fetcherId={layout.fetcher}
|
||||
options={blockOptions}
|
||||
blockDef={blockDef}
|
||||
label={layout.label}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>
|
||||
{layout.helperText}
|
||||
</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (layout?.input === 'variableDropdown') {
|
||||
return (
|
||||
<VariableSearchInput
|
||||
initialVariableId={data}
|
||||
onSelectVariable={(variable) => onDataChange(variable?.id)}
|
||||
placeholder={layout?.placeholder}
|
||||
label={layout?.label}
|
||||
moreInfoTooltip={layout.moreInfoTooltip}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>
|
||||
{layout.helperText}
|
||||
</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (layout?.input === 'textarea') {
|
||||
return (
|
||||
<Textarea
|
||||
defaultValue={data ?? layout?.defaultValue}
|
||||
label={layout?.label}
|
||||
placeholder={layout?.placeholder}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>
|
||||
{layout.helperText}
|
||||
</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
isRequired={layout?.isRequired}
|
||||
withVariableButton={layout?.withVariableButton}
|
||||
moreInfoTooltip={layout.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
defaultValue={data ?? layout?.defaultValue}
|
||||
label={layout?.label}
|
||||
placeholder={layout?.placeholder}
|
||||
helperText={
|
||||
layout?.helperText ? (
|
||||
<Markdown components={mdComponents}>{layout.helperText}</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
type={layout?.input === 'password' ? 'password' : undefined}
|
||||
isRequired={layout?.isRequired}
|
||||
withVariableButton={layout?.withVariableButton}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ZodArrayContent = ({
|
||||
schema,
|
||||
data,
|
||||
layout,
|
||||
isInAccordion,
|
||||
onDataChange,
|
||||
}: {
|
||||
schema: z.ZodTypeAny
|
||||
data: any
|
||||
layout: ZodLayoutMetadata<ZodTypeAny> | undefined
|
||||
isInAccordion?: boolean
|
||||
onDataChange: (val: any) => void
|
||||
}) => (
|
||||
<TableList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
isOrdered={layout?.isOrdered}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
@ -0,0 +1,122 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
import { ZodLayoutMetadata } from '@typebot.io/forge/zod'
|
||||
import { ReactNode } from 'react'
|
||||
import { ZodTypeAny } from 'zod'
|
||||
import { ZodFieldLayout } from './ZodFieldLayout'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
|
||||
export const ZodObjectLayout = ({
|
||||
schema,
|
||||
data,
|
||||
isInAccordion,
|
||||
ignoreKeys,
|
||||
blockDef,
|
||||
blockOptions,
|
||||
onDataChange,
|
||||
}: {
|
||||
schema: z.ZodObject<any>
|
||||
data: any
|
||||
isInAccordion?: boolean
|
||||
ignoreKeys?: string[]
|
||||
blockDef?: ForgedBlockDefinition
|
||||
blockOptions?: ForgedBlock['options']
|
||||
onDataChange: (value: any) => void
|
||||
}) => {
|
||||
return Object.keys(schema.shape).reduce<{
|
||||
nodes: ReactNode[]
|
||||
accordionsCreated: string[]
|
||||
}>(
|
||||
(nodes, key, index) => {
|
||||
if (ignoreKeys?.includes(key)) return nodes
|
||||
const keySchema = schema.shape[key]
|
||||
const layout = keySchema._def.layout as
|
||||
| ZodLayoutMetadata<ZodTypeAny>
|
||||
| undefined
|
||||
if (
|
||||
layout &&
|
||||
layout.accordion &&
|
||||
!isInAccordion &&
|
||||
keySchema._def.innerType._def.typeName !== 'ZodArray'
|
||||
) {
|
||||
if (nodes.accordionsCreated.includes(layout.accordion)) return nodes
|
||||
const accordionKeys = getObjectKeysWithSameAccordionAttr(
|
||||
layout.accordion,
|
||||
schema
|
||||
)
|
||||
return {
|
||||
nodes: [
|
||||
...nodes.nodes,
|
||||
<Accordion allowToggle key={layout.accordion}>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
{layout.accordion}
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel as={Stack} spacing={4}>
|
||||
{accordionKeys.map((accordionKey, idx) => (
|
||||
<ZodFieldLayout
|
||||
key={accordionKey + idx}
|
||||
schema={schema.shape[accordionKey]}
|
||||
data={data?.[accordionKey]}
|
||||
onDataChange={(val) =>
|
||||
onDataChange({ ...data, [accordionKey]: val })
|
||||
}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
isInAccordion
|
||||
/>
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
],
|
||||
accordionsCreated: [
|
||||
...nodes.accordionsCreated,
|
||||
layout.accordion as string,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: [
|
||||
...nodes.nodes,
|
||||
<ZodFieldLayout
|
||||
schema={keySchema}
|
||||
key={index}
|
||||
data={data?.[key]}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
onDataChange={(val) => onDataChange({ ...data, [key]: val })}
|
||||
/>,
|
||||
],
|
||||
accordionsCreated: nodes.accordionsCreated,
|
||||
}
|
||||
},
|
||||
{ nodes: [], accordionsCreated: [] }
|
||||
).nodes
|
||||
}
|
||||
|
||||
const getObjectKeysWithSameAccordionAttr = (
|
||||
accordion: string,
|
||||
schema: z.ZodObject<any>
|
||||
) =>
|
||||
Object.keys(schema.shape).reduce<string[]>((keys, currentKey) => {
|
||||
const l = schema.shape[currentKey]._def.layout as
|
||||
| ZodLayoutMetadata<ZodTypeAny>
|
||||
| undefined
|
||||
return !l?.accordion || l.accordion !== accordion
|
||||
? keys
|
||||
: [...keys, currentKey]
|
||||
}, [])
|
27
apps/builder/src/features/forge/hooks/useForgedBlock.ts
Normal file
27
apps/builder/src/features/forge/hooks/useForgedBlock.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { useMemo } from 'react'
|
||||
import { forgedBlockSchemas, forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||
|
||||
export const useForgedBlock = (
|
||||
blockType: BlockWithOptions['type'],
|
||||
action?: string
|
||||
) =>
|
||||
useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((enabledBlocks as any).includes(blockType) === false) return {}
|
||||
const blockDef = forgedBlocks.find(
|
||||
(b) => enabledBlocks.includes(b.id) && b.id === blockType
|
||||
)
|
||||
return {
|
||||
blockDef,
|
||||
blockSchema: forgedBlockSchemas.find(
|
||||
(b) =>
|
||||
enabledBlocks.includes(b.shape.type.value) &&
|
||||
b.shape.type.value === blockType
|
||||
),
|
||||
actionDef: action
|
||||
? blockDef?.actions.find((a) => a.name === action)
|
||||
: undefined,
|
||||
}
|
||||
}, [action, blockType])
|
@ -210,7 +210,7 @@ export const BlockNode = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
data-testid={`block`}
|
||||
data-testid={`block ${block.id}`}
|
||||
w="full"
|
||||
className="prevent-group-drag"
|
||||
>
|
||||
@ -234,11 +234,7 @@ export const BlockNode = ({
|
||||
w="full"
|
||||
transition="border-color 0.2s"
|
||||
>
|
||||
<BlockIcon
|
||||
type={block.type}
|
||||
mt="1"
|
||||
data-testid={`${block.id}-icon`}
|
||||
/>
|
||||
<BlockIcon type={block.type} mt=".25rem" />
|
||||
{typebot?.groups[indices.groupIndex].id && (
|
||||
<BlockNodeContent
|
||||
block={block}
|
||||
|
@ -3,7 +3,6 @@ import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNod
|
||||
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
|
||||
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'
|
||||
import { JumpNodeBody } from '@/features/blocks/logic/jump/components/JumpNodeBody'
|
||||
import { OpenAINodeBody } from '@/features/blocks/integrations/openai/components/OpenAINodeBody'
|
||||
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio/components/AudioBubbleNode'
|
||||
import { EmbedBubbleContent } from '@/features/blocks/bubbles/embed/components/EmbedBubbleContent'
|
||||
import { ImageBubbleContent } from '@/features/blocks/bubbles/image/components/ImageBubbleContent'
|
||||
@ -38,6 +37,8 @@ import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/con
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { ForgedBlockNodeContent } from '@/features/forge/components/ForgedBlockNodeContent'
|
||||
import { OpenAINodeBody } from '@/features/blocks/integrations/openai/components/OpenAINodeBody'
|
||||
|
||||
type Props = {
|
||||
block: BlockV6
|
||||
@ -153,5 +154,8 @@ export const BlockNodeContent = ({
|
||||
case IntegrationBlockType.ZEMANTIC_AI: {
|
||||
return <ZemanticAiNodeBody options={block.options} />
|
||||
}
|
||||
default: {
|
||||
return <ForgedBlockNodeContent block={block} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||
import { getHelpDocUrl } from '@/features/graph/helpers/getHelpDocUrl'
|
||||
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
|
||||
|
||||
type Props = {
|
||||
blockType: BlockWithOptions['type']
|
||||
@ -15,7 +16,8 @@ type Props = {
|
||||
}
|
||||
|
||||
export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
||||
const helpDocUrl = getHelpDocUrl(blockType)
|
||||
const { blockDef } = useForgedBlock(blockType)
|
||||
const helpDocUrl = getHelpDocUrl(blockType, blockDef)
|
||||
return (
|
||||
<HStack
|
||||
rounded="md"
|
||||
@ -34,17 +36,19 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
||||
onClick={onExpandClick}
|
||||
size="xs"
|
||||
/>
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<BuoyIcon />}
|
||||
borderLeftRadius="none"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
href={helpDocUrl}
|
||||
isExternal
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
{helpDocUrl && (
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<BuoyIcon />}
|
||||
borderLeftRadius="none"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
href={helpDocUrl}
|
||||
isExternal
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import { ScriptSettings } from '@/features/blocks/logic/script/components/Script
|
||||
import { JumpSettings } from '@/features/blocks/logic/jump/components/JumpSettings'
|
||||
import { MakeComSettings } from '@/features/blocks/integrations/makeCom/components/MakeComSettings'
|
||||
import { PabblyConnectSettings } from '@/features/blocks/integrations/pabbly/components/PabblyConnectSettings'
|
||||
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
|
||||
import { ButtonsBlockSettings } from '@/features/blocks/inputs/buttons/components/ButtonsBlockSettings'
|
||||
import { FileInputSettings } from '@/features/blocks/inputs/fileUpload/components/FileInputSettings'
|
||||
import { PaymentSettings } from '@/features/blocks/inputs/payment/components/PaymentSettings'
|
||||
@ -44,6 +43,8 @@ import { ZemanticAiSettings } from '@/features/blocks/integrations/zemanticAi/Ze
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { ForgedBlockSettings } from '../../../../forge/components/ForgedBlockSettings'
|
||||
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
@ -323,5 +324,10 @@ export const BlockSettings = ({
|
||||
}
|
||||
case LogicBlockType.CONDITION:
|
||||
return null
|
||||
default: {
|
||||
return (
|
||||
<ForgedBlockSettings block={block} onOptionsChange={updateOptions} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-schemas'
|
||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
|
||||
export const getHelpDocUrl = (
|
||||
blockType: BlockWithOptions['type'],
|
||||
blockDef?: ForgedBlockDefinition
|
||||
): string | undefined => {
|
||||
switch (blockType) {
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return 'https://docs.typebot.io/editor/blocks/logic/typebot-link'
|
||||
@ -65,5 +69,7 @@ export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
|
||||
return 'https://docs.typebot.io/editor/blocks/integrations/zemantic-ai'
|
||||
case LogicBlockType.CONDITION:
|
||||
return 'https://docs.typebot.io/editor/blocks/logic/condition'
|
||||
default:
|
||||
return blockDef?.docsUrl
|
||||
}
|
||||
}
|
||||
|
@ -253,7 +253,6 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
||||
[]
|
||||
}
|
||||
onItemsChange={updateStartConditionComparisons}
|
||||
Item={WhatsAppComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList
|
||||
@ -270,7 +269,9 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add a comparison"
|
||||
/>
|
||||
>
|
||||
{(props) => <WhatsAppComparisonItem {...props} />}
|
||||
</TableList>
|
||||
</SwitchWithRelatedSettings>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
@ -46,6 +46,7 @@ export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
|
||||
withVariableButton={false}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
direction="row"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max delay (in seconds):"
|
||||
@ -58,6 +59,7 @@ export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
|
||||
withVariableButton={false}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
direction="row"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { forgedBlockSchemas } from '@typebot.io/forge-schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import { Block, Typebot } from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
|
||||
|
||||
@ -49,37 +50,30 @@ const sanitizeBlock =
|
||||
(workspaceId: string) =>
|
||||
async (block: Block): Promise<Block> => {
|
||||
if (!('options' in block) || !block.options) return block
|
||||
|
||||
if (enabledBlocks.includes(block.type as (typeof enabledBlocks)[number])) {
|
||||
const schema = forgedBlockSchemas.find(
|
||||
(s) => s.shape.type.value === block.type
|
||||
)
|
||||
if (!schema)
|
||||
throw new Error(
|
||||
`Integration block schema not found for block type ${block.type}`
|
||||
)
|
||||
return schema.parse({
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options.credentialsId
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!('credentialsId' in block.options) || !block.options.credentialsId)
|
||||
return block
|
||||
|
||||
switch (block.type) {
|
||||
case InputBlockType.PAYMENT:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options?.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options?.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options?.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return {
|
||||
...block,
|
||||
@ -92,7 +86,15 @@ const sanitizeBlock =
|
||||
},
|
||||
}
|
||||
default:
|
||||
return block
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options?.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import { generateUploadUrl } from '@/features/upload/api/generateUploadUrl'
|
||||
import { openAIRouter } from '@/features/blocks/integrations/openai/api/router'
|
||||
import { whatsAppRouter } from '@/features/whatsapp/router'
|
||||
import { zemanticAiRouter } from '@/features/blocks/integrations/zemanticAi/api/router'
|
||||
import { forgedCredentialsRouter } from '@/features/forge/api/credentials/router'
|
||||
import { integrationsRouter } from '@/features/forge/api/router'
|
||||
|
||||
export const internalRouter = router({
|
||||
getAppVersionProcedure,
|
||||
@ -11,6 +13,8 @@ export const internalRouter = router({
|
||||
whatsApp: whatsAppRouter,
|
||||
openAI: openAIRouter,
|
||||
zemanticAI: zemanticAiRouter,
|
||||
integrationCredentials: forgedCredentialsRouter,
|
||||
integrations: integrationsRouter,
|
||||
})
|
||||
|
||||
export type InternalRouter = typeof internalRouter
|
||||
|
@ -8,6 +8,7 @@ import '@/assets/styles/routerProgressBar.css'
|
||||
import '@/assets/styles/plate.css'
|
||||
import '@/assets/styles/resultsTable.css'
|
||||
import '@/assets/styles/custom.css'
|
||||
import '@/assets/styles/md.css'
|
||||
import { UserProvider } from '@/features/account/UserProvider'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SupportBubble } from '@/components/SupportBubble'
|
||||
|
@ -24,7 +24,7 @@ export default function Page() {
|
||||
justifyContent="center"
|
||||
spacing={4}
|
||||
>
|
||||
<AlertIcon fontSize="4xl" />
|
||||
<AlertIcon width="40px" />
|
||||
<Heading fontSize="2xl">Your workspace has unpaid invoice(s).</Heading>
|
||||
<Text>Head over to the billing portal to pay it.</Text>
|
||||
{workspace?.id && (
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -46,7 +46,7 @@
|
||||
"devDependencies": {
|
||||
"@algolia/client-search": "4.15.0",
|
||||
"@docusaurus/types": "2.3.1",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react": "18.2.15",
|
||||
"dotenv-cli": "7.2.1",
|
||||
"tsx": "3.14.0",
|
||||
"typescript": "5.3.2",
|
||||
|
@ -20,7 +20,7 @@
|
||||
"aos": "2.3.4",
|
||||
"focus-visible": "5.2.0",
|
||||
"framer-motion": "10.12.20",
|
||||
"next": "13.5.4",
|
||||
"next": "14.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
@ -44,6 +44,20 @@ const nextConfig = {
|
||||
experimental: {
|
||||
outputFileTracingRoot: join(__dirname, '../../'),
|
||||
},
|
||||
webpack: (config, { nextRuntime }) => {
|
||||
if (nextRuntime === 'nodejs') return config
|
||||
|
||||
if (nextRuntime === 'edge') {
|
||||
config.resolve.alias['minio'] = false
|
||||
config.resolve.alias['got'] = false
|
||||
return config
|
||||
}
|
||||
// These packages are imports from the integrations definition files that can be ignored for the client.
|
||||
config.resolve.alias['minio'] = false
|
||||
config.resolve.alias['got'] = false
|
||||
config.resolve.alias['openai'] = false
|
||||
return config
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
|
@ -23,7 +23,7 @@
|
||||
"cors": "2.8.5",
|
||||
"google-spreadsheet": "4.0.2",
|
||||
"got": "12.6.0",
|
||||
"next": "13.5.4",
|
||||
"next": "14.0.3",
|
||||
"nextjs-cors": "2.1.2",
|
||||
"nodemailer": "6.9.3",
|
||||
"openai": "4.19.0",
|
||||
@ -42,6 +42,10 @@
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/schemas": "workspace:*",
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@typebot.io/forge-schemas": "workspace:*",
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*",
|
||||
"@typebot.io/forge-repository": "workspace:*",
|
||||
"@types/cors": "2.8.13",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/nodemailer": "6.4.8",
|
||||
|
@ -2,11 +2,19 @@ import { connect } from '@planetscale/database'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { SessionState } from '@typebot.io/schemas'
|
||||
import { StreamingTextResponse } from 'ai'
|
||||
import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrations/openai/getChatCompletionStream'
|
||||
import OpenAI from 'openai'
|
||||
import { NextResponse } from 'next/dist/server/web/spec-extension/response'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||
import { forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { decryptV2 } from '@typebot.io/lib/api/encryption/decryptV2'
|
||||
import { ReadOnlyVariableStore } from '@typebot.io/forge'
|
||||
import {
|
||||
ParseVariablesOptions,
|
||||
parseVariables,
|
||||
} from '@typebot.io/variables/parseVariables'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrations/legacy/openai/getChatCompletionStream'
|
||||
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/schema'
|
||||
|
||||
export const runtime = 'edge'
|
||||
export const preferredRegion = 'lhr1'
|
||||
@ -31,8 +39,8 @@ export async function OPTIONS() {
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { sessionId, messages } = (await req.json()) as {
|
||||
messages: OpenAI.Chat.ChatCompletionMessage[] | undefined
|
||||
sessionId: string
|
||||
messages: OpenAI.Chat.ChatCompletionMessage[]
|
||||
}
|
||||
|
||||
if (!sessionId)
|
||||
@ -41,12 +49,6 @@ export async function POST(req: Request) {
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
if (!messages)
|
||||
return NextResponse.json(
|
||||
{ message: 'No messages provided' },
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
const conn = connect({ url: env.DATABASE_URL })
|
||||
|
||||
const chatSession = await conn.execute(
|
||||
@ -73,21 +75,79 @@ export async function POST(req: Request) {
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
if (
|
||||
block.type !== IntegrationBlockType.OPEN_AI ||
|
||||
block.options?.task !== 'Create chat completion'
|
||||
)
|
||||
if (!('options' in block))
|
||||
return NextResponse.json(
|
||||
{ message: 'Current block is not an OpenAI block' },
|
||||
{ message: 'Current block does not have options' },
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
if (block.type === IntegrationBlockType.OPEN_AI && messages) {
|
||||
try {
|
||||
const stream = await getChatCompletionStream(conn)(
|
||||
state,
|
||||
block.options as ChatCompletionOpenAIOptions,
|
||||
messages
|
||||
)
|
||||
if (!stream)
|
||||
return NextResponse.json(
|
||||
{ message: 'Could not create stream' },
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
return new StreamingTextResponse(stream, {
|
||||
headers: responseHeaders,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAI.APIError) {
|
||||
const { name, status, message } = error
|
||||
return NextResponse.json(
|
||||
{ name, status, message },
|
||||
{ status, headers: responseHeaders }
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
const blockDef = forgedBlocks.find((b) => b.id === block.type)
|
||||
const action = blockDef?.actions.find((a) => a.name === block.options?.action)
|
||||
|
||||
if (!action || !action.run?.stream)
|
||||
return NextResponse.json(
|
||||
{ message: 'This action does not have a stream function' },
|
||||
{ status: 400, headers: responseHeaders }
|
||||
)
|
||||
|
||||
try {
|
||||
const stream = await getChatCompletionStream(conn)(
|
||||
state,
|
||||
block.options,
|
||||
messages
|
||||
if (!block.options.credentialsId) return
|
||||
const credentials = (
|
||||
await conn.execute('select data, iv from Credentials where id=?', [
|
||||
block.options.credentialsId,
|
||||
])
|
||||
).rows.at(0) as { data: string; iv: string } | undefined
|
||||
if (!credentials) {
|
||||
console.error('Could not find credentials in database')
|
||||
return
|
||||
}
|
||||
const decryptedCredentials = await decryptV2(
|
||||
credentials.data,
|
||||
credentials.iv
|
||||
)
|
||||
const variables: ReadOnlyVariableStore = {
|
||||
get: (id: string) => {
|
||||
const variable = state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === id
|
||||
)
|
||||
return variable?.value
|
||||
},
|
||||
parse: (text: string, params?: ParseVariablesOptions) =>
|
||||
parseVariables(state.typebotsQueue[0].typebot.variables, params)(text),
|
||||
}
|
||||
const stream = await action.run.stream.run({
|
||||
credentials: decryptedCredentials,
|
||||
options: block.options,
|
||||
variables,
|
||||
})
|
||||
if (!stream)
|
||||
return NextResponse.json(
|
||||
{ message: 'Could not create stream' },
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { parseVariables } from '@typebot.io/bot-engine/variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
@ -18,7 +18,7 @@ import Cors from 'cors'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { fetchLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots'
|
||||
import { getPreviouslyLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
|
||||
import { parseVariables } from '@typebot.io/bot-engine/variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog'
|
||||
import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult'
|
||||
|
@ -15,7 +15,8 @@
|
||||
"generate-change-log": "git fetch --all && pnpx gitmoji-changelog",
|
||||
"locales:sync": "tolgee sync './apps/builder/src/**/*.ts?(x)' --continue-on-warning --remove-unused",
|
||||
"locales:push": "tolgee push ./apps/builder/public/locales",
|
||||
"locales:pull": "tolgee pull ./apps/builder/public/locales"
|
||||
"locales:pull": "tolgee pull ./apps/builder/public/locales",
|
||||
"create-new-block": "cd packages/forge/cli && pnpm start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
|
@ -5,9 +5,9 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { filterChoiceItems } from './filterChoiceItems'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
|
||||
export const injectVariableValuesInButtonsInputBlock =
|
||||
(state: SessionState) =>
|
||||
@ -38,7 +38,7 @@ const getVariableValue =
|
||||
(variable: VariableWithValue): (string | null)[] => {
|
||||
if (!Array.isArray(variable.value)) {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const [transformedVariable] = transformStringVariablesToList(variables)([
|
||||
const [transformedVariable] = transformVariablesToList(variables)([
|
||||
variable.id,
|
||||
])
|
||||
updateVariablesInSession(state)([transformedVariable])
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getPrefilledInputValue } from '../../../getPrefilledValue'
|
||||
import { DateInputBlock, SessionState, Variable } from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const parseDateInput =
|
||||
(state: SessionState) => (block: DateInputBlock) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
import { NumberInputBlock, Variable } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const validateNumber = (
|
||||
inputValue: string,
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import Stripe from 'stripe'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
|
||||
export const injectVariableValuesInPictureChoiceBlock =
|
||||
(variables: Variable[]) =>
|
||||
|
@ -2,9 +2,9 @@ import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { ChatwootBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { defaultChatwootOptions } from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
|
||||
|
||||
const parseSetUserCode = (
|
||||
|
@ -8,8 +8,8 @@ import { isNotEmpty, byId, isDefined } from '@typebot.io/lib'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
|
||||
export const getRow = async (
|
||||
state: SessionState,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Variable, Cell } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const parseCellValues =
|
||||
(variables: Variable[]) =>
|
||||
|
@ -7,7 +7,7 @@ import { parseCellValues } from './helpers/parseCellValues'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
|
||||
export const updateRow = async (
|
||||
state: SessionState,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { ExecuteIntegrationResponse } from '../../../../types'
|
||||
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
|
||||
export const executeGoogleAnalyticsBlock = (
|
||||
state: SessionState,
|
@ -7,12 +7,12 @@ import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
|
||||
import { ExecuteIntegrationResponse } from '../../../../types'
|
||||
import { ExecuteIntegrationResponse } from '../../../../../types'
|
||||
import OpenAI, { ClientOptions } from 'openai'
|
||||
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
||||
import { updateVariablesInSession } from '../../../../variables/updateVariablesInSession'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { parseVariables } from '../../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const createSpeechOpenAI = async (
|
||||
state: SessionState,
|
@ -15,9 +15,9 @@ import { parseChatCompletionMessages } from './parseChatCompletionMessages'
|
||||
import { executeChatCompletionOpenAIRequest } from './executeChatCompletionOpenAIRequest'
|
||||
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { ExecuteIntegrationResponse } from '../../../../types'
|
||||
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import {
|
||||
chatCompletionMessageRoles,
|
||||
defaultOpenAIOptions,
|
@ -1,7 +1,7 @@
|
||||
import { SessionState } from '@typebot.io/schemas'
|
||||
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { createChatCompletionOpenAI } from './createChatCompletionOpenAI'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { ExecuteIntegrationResponse } from '../../../../types'
|
||||
import { createSpeechOpenAI } from './audio/createSpeechOpenAI'
|
||||
|
||||
export const executeOpenAIBlock = async (
|
@ -7,7 +7,7 @@ import {
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||
import { OpenAIStream } from 'ai'
|
||||
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
|
||||
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
|
||||
import { ClientOptions, OpenAI } from 'openai'
|
||||
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
|
||||
|
@ -2,8 +2,8 @@ import { byId, isNotEmpty } from '@typebot.io/lib'
|
||||
import { Variable, VariableWithValue } from '@typebot.io/schemas'
|
||||
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import type { OpenAI } from 'openai'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
|
||||
|
||||
export const parseChatCompletionMessages =
|
||||
(variables: Variable[]) =>
|
||||
@ -24,7 +24,7 @@ export const parseChatCompletionMessages =
|
||||
)
|
||||
return
|
||||
variablesTransformedToList.push(
|
||||
...transformStringVariablesToList(variables)([
|
||||
...transformVariablesToList(variables)([
|
||||
message.content.assistantMessagesVariableId,
|
||||
message.content.userMessagesVariableId,
|
||||
])
|
@ -2,7 +2,7 @@ import { byId, isDefined } from '@typebot.io/lib'
|
||||
import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
|
||||
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
|
||||
export const resumeChatCompletion =
|
||||
(
|
@ -1,6 +1,6 @@
|
||||
import { PixelBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
|
||||
export const executePixelBlock = (
|
||||
state: SessionState,
|
||||
|
@ -14,11 +14,11 @@ import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
||||
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { defaultFrom, defaultTransportOptions } from './constants'
|
||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
|
||||
|
||||
export const executeSendEmailBlock = async (
|
||||
|
@ -18,7 +18,7 @@ import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||
import got, { Method, HTTPError, OptionsInit } from 'got'
|
||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import {
|
||||
HttpMethod,
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
|
||||
type Props = {
|
||||
state: SessionState
|
||||
|
@ -10,7 +10,7 @@ import { byId, isDefined, isEmpty } from '@typebot.io/lib'
|
||||
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
|
||||
const URL = 'https://api.zemantic.ai/v1/search-documents'
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
||||
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
|
||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import {
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RedirectBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { sanitizeUrl } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const executeRedirect = (
|
||||
state: SessionState,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
|
||||
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
|
||||
export const executeScript = (
|
||||
state: SessionState,
|
||||
|
@ -2,9 +2,9 @@ import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
export const executeSetVariable = (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
export const executeWait = (
|
||||
|
@ -12,7 +12,6 @@ import { getNextGroup } from './getNextGroup'
|
||||
import { validateEmail } from './blocks/inputs/email/validateEmail'
|
||||
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
|
||||
import { validateUrl } from './blocks/inputs/url/validateUrl'
|
||||
import { resumeChatCompletion } from './blocks/integrations/openai/resumeChatCompletion'
|
||||
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
|
||||
import { upsertAnswer } from './queries/upsertAnswer'
|
||||
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
|
||||
@ -21,8 +20,8 @@ import { validateNumber } from './blocks/inputs/number/validateNumber'
|
||||
import { parseDateReply } from './blocks/inputs/date/parseDateReply'
|
||||
import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
|
||||
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
|
||||
import { parseVariables } from './variables/parseVariables'
|
||||
import { updateVariablesInSession } from './variables/updateVariablesInSession'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { startBotFlow } from './startBotFlow'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { parseNumber } from './blocks/inputs/number/parseNumber'
|
||||
@ -37,6 +36,9 @@ import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks
|
||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
|
||||
|
||||
type Params = {
|
||||
version: 1 | 2
|
||||
@ -80,14 +82,9 @@ export const continueBotFlow = async (
|
||||
}
|
||||
newSessionState = updateVariablesInSession(state)([newVariable])
|
||||
}
|
||||
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
|
||||
const result = resumeWebhookExecution({
|
||||
state,
|
||||
block,
|
||||
response: JSON.parse(reply),
|
||||
})
|
||||
if (result.newSessionState) newSessionState = result.newSessionState
|
||||
} else if (
|
||||
}
|
||||
// Legacy
|
||||
else if (
|
||||
block.type === IntegrationBlockType.OPEN_AI &&
|
||||
block.options?.task === 'Create chat completion'
|
||||
) {
|
||||
@ -99,6 +96,58 @@ export const continueBotFlow = async (
|
||||
})(reply)
|
||||
newSessionState = result.newSessionState
|
||||
}
|
||||
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
|
||||
const result = resumeWebhookExecution({
|
||||
state,
|
||||
block,
|
||||
response: JSON.parse(reply),
|
||||
})
|
||||
if (result.newSessionState) newSessionState = result.newSessionState
|
||||
} else if (
|
||||
enabledBlocks.includes(block.type as (typeof enabledBlocks)[number])
|
||||
) {
|
||||
if (reply) {
|
||||
const options = (block as ForgedBlock).options
|
||||
const action = forgedBlocks
|
||||
.find((b) => b.id === block.type)
|
||||
?.actions.find((a) => a.name === options?.action)
|
||||
if (action) {
|
||||
if (action.run?.stream?.getStreamVariableId) {
|
||||
firstBubbleWasStreamed = true
|
||||
const variableToUpdate =
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
|
||||
)
|
||||
if (variableToUpdate)
|
||||
newSessionState = updateVariablesInSession(state)([
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: reply,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
if (
|
||||
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
|
||||
) {
|
||||
const variableToUpdate =
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) =>
|
||||
v.id ===
|
||||
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
|
||||
options
|
||||
)
|
||||
)
|
||||
if (variableToUpdate)
|
||||
newSessionState = updateVariablesInSession(state)([
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: reply,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let formattedReply: string | undefined
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
createId,
|
||||
isBubbleBlock,
|
||||
isInputBlock,
|
||||
isIntegrationBlock,
|
||||
@ -20,7 +21,7 @@ import { injectVariableValuesInButtonsInputBlock } from './blocks/inputs/buttons
|
||||
import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock'
|
||||
import { getPrefilledInputValue } from './getPrefilledValue'
|
||||
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
|
||||
import { deepParseVariables } from './variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import {
|
||||
BubbleBlockWithDefinedContent,
|
||||
parseBubbleBlock,
|
||||
@ -139,10 +140,21 @@ export const executeGroup = async (
|
||||
lastBubbleBlockId,
|
||||
})),
|
||||
]
|
||||
if (
|
||||
'customEmbedBubble' in executionResponse &&
|
||||
executionResponse.customEmbedBubble
|
||||
) {
|
||||
messages.push({
|
||||
id: createId(),
|
||||
...executionResponse.customEmbedBubble,
|
||||
})
|
||||
}
|
||||
if (
|
||||
executionResponse.clientSideActions?.find(
|
||||
(action) => action.expectsDedicatedReply
|
||||
)
|
||||
) ||
|
||||
('customEmbedBubble' in executionResponse &&
|
||||
executionResponse.customEmbedBubble)
|
||||
) {
|
||||
return {
|
||||
messages,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { executeOpenAIBlock } from './blocks/integrations/openai/executeOpenAIBlock'
|
||||
import { executeSendEmailBlock } from './blocks/integrations/sendEmail/executeSendEmailBlock'
|
||||
import { executeWebhookBlock } from './blocks/integrations/webhook/executeWebhookBlock'
|
||||
import { executeChatwootBlock } from './blocks/integrations/chatwoot/executeChatwootBlock'
|
||||
import { executeGoogleAnalyticsBlock } from './blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
|
||||
import { executeGoogleAnalyticsBlock } from './blocks/integrations/legacy/googleAnalytics/executeGoogleAnalyticsBlock'
|
||||
import { executeGoogleSheetBlock } from './blocks/integrations/googleSheets/executeGoogleSheetBlock'
|
||||
import { executePixelBlock } from './blocks/integrations/pixel/executePixelBlock'
|
||||
import { executeZemanticAiBlock } from './blocks/integrations/zemanticAi/executeZemanticAiBlock'
|
||||
import { IntegrationBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteIntegrationResponse } from './types'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { executeOpenAIBlock } from './blocks/integrations/legacy/openai/executeOpenAIBlock'
|
||||
import { executeForgedBlock } from './forge/executeForgedBlock'
|
||||
|
||||
export const executeIntegration =
|
||||
(state: SessionState) =>
|
||||
@ -33,5 +34,7 @@ export const executeIntegration =
|
||||
return executePixelBlock(state, block)
|
||||
case IntegrationBlockType.ZEMANTIC_AI:
|
||||
return executeZemanticAiBlock(state, block)
|
||||
default:
|
||||
return executeForgedBlock(state, block)
|
||||
}
|
||||
}
|
||||
|
207
packages/bot-engine/forge/executeForgedBlock.ts
Normal file
207
packages/bot-engine/forge/executeForgedBlock.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { VariableStore, LogsStore } from '@typebot.io/forge'
|
||||
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import {
|
||||
SessionState,
|
||||
ContinueChatResponse,
|
||||
Block,
|
||||
TypebotInSession,
|
||||
} from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import {
|
||||
ParseVariablesOptions,
|
||||
parseVariables,
|
||||
} from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { ExecuteIntegrationResponse } from '../types'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
|
||||
export const executeForgedBlock = async (
|
||||
state: SessionState,
|
||||
block: ForgedBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const blockDef = forgedBlocks.find((b) => b.id === block.type)
|
||||
if (!blockDef) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const action = blockDef.actions.find((a) => a.name === block.options.action)
|
||||
const noCredentialsError = {
|
||||
status: 'error',
|
||||
description: 'Credentials not provided for integration',
|
||||
}
|
||||
|
||||
let credentials: { data: string; iv: string } | null = null
|
||||
if (blockDef.auth) {
|
||||
if (!block.options.credentialsId) {
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: [noCredentialsError],
|
||||
}
|
||||
}
|
||||
credentials = await prisma.credentials.findUnique({
|
||||
where: {
|
||||
id: block.options.credentialsId,
|
||||
},
|
||||
})
|
||||
if (!credentials) {
|
||||
console.error('Could not find credentials in database')
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: [noCredentialsError],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typebot = state.typebotsQueue[0].typebot
|
||||
if (
|
||||
action?.run?.stream &&
|
||||
isPlaneteScale() &&
|
||||
credentials &&
|
||||
isCredentialsV2(credentials) &&
|
||||
state.isStreamEnabled &&
|
||||
!state.whatsApp &&
|
||||
isNextBubbleTextWithStreamingVar(typebot)(
|
||||
block.id,
|
||||
action.run.stream.getStreamVariableId(block.options)
|
||||
)
|
||||
) {
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
{
|
||||
expectsDedicatedReply: true,
|
||||
stream: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
let newSessionState = state
|
||||
|
||||
const variables: VariableStore = {
|
||||
get: (id: string) => {
|
||||
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === id
|
||||
)
|
||||
return variable?.value
|
||||
},
|
||||
set: (id: string, value: unknown) => {
|
||||
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === id
|
||||
)
|
||||
if (!variable) return
|
||||
newSessionState = updateVariablesInSession(newSessionState)([
|
||||
{ ...variable, value },
|
||||
])
|
||||
},
|
||||
parse: (text: string, params?: ParseVariablesOptions) =>
|
||||
parseVariables(
|
||||
newSessionState.typebotsQueue[0].typebot.variables,
|
||||
params
|
||||
)(text),
|
||||
}
|
||||
let logs: NonNullable<ContinueChatResponse['logs']> = []
|
||||
const logsStore: LogsStore = {
|
||||
add: (log) => {
|
||||
if (typeof log === 'string') {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: log,
|
||||
})
|
||||
return
|
||||
}
|
||||
logs.push(log)
|
||||
},
|
||||
}
|
||||
const credentialsData = credentials
|
||||
? await decrypt(credentials.data, credentials.iv)
|
||||
: undefined
|
||||
|
||||
const parsedOptions = deepParseVariables(
|
||||
state.typebotsQueue[0].typebot.variables
|
||||
)(block.options)
|
||||
await action?.run?.server?.({
|
||||
credentials: credentialsData ?? {},
|
||||
options: parsedOptions,
|
||||
variables,
|
||||
logs: logsStore,
|
||||
})
|
||||
|
||||
const clientSideActions: ExecuteIntegrationResponse['clientSideActions'] = []
|
||||
|
||||
if (
|
||||
action?.run?.web?.parseFunction &&
|
||||
(state.typebotsQueue[0].resultId || !blockDef.isDisabledInPreview)
|
||||
) {
|
||||
clientSideActions.push({
|
||||
codeToExecute: action?.run?.web?.parseFunction({
|
||||
options: parsedOptions,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
newSessionState,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs,
|
||||
clientSideActions,
|
||||
customEmbedBubble: action?.run?.web?.displayEmbedBubble
|
||||
? {
|
||||
type: 'custom-embed',
|
||||
content: {
|
||||
initFunction: action.run.web.displayEmbedBubble.parseInitFunction({
|
||||
options: parsedOptions,
|
||||
}),
|
||||
waitForEventFunction:
|
||||
action.run.web.displayEmbedBubble.waitForEvent?.parseFunction?.({
|
||||
options: parsedOptions,
|
||||
}),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const isNextBubbleTextWithStreamingVar =
|
||||
(typebot: TypebotInSession) =>
|
||||
(blockId: string, streamVariableId?: string): boolean => {
|
||||
const streamVariable = typebot.variables.find(
|
||||
(variable) => variable.id === streamVariableId
|
||||
)
|
||||
if (!streamVariable) return false
|
||||
const nextBlock = getNextBlock(typebot)(blockId)
|
||||
if (!nextBlock) return false
|
||||
return (
|
||||
nextBlock.type === BubbleBlockType.TEXT &&
|
||||
(nextBlock.content?.richText?.length ?? 0) > 0 &&
|
||||
nextBlock.content?.richText?.at(0)?.children.at(0).text ===
|
||||
`{{${streamVariable.name}}}`
|
||||
)
|
||||
}
|
||||
|
||||
const getNextBlock =
|
||||
(typebot: TypebotInSession) =>
|
||||
(blockId: string): Block | undefined => {
|
||||
const group = typebot.groups.find((group) =>
|
||||
group.blocks.find(byId(blockId))
|
||||
)
|
||||
if (!group) return
|
||||
const blockIndex = group.blocks.findIndex(byId(blockId))
|
||||
const nextBlockInGroup = group.blocks.at(blockIndex + 1)
|
||||
if (nextBlockInGroup) return nextBlockInGroup
|
||||
const outgoingEdgeId = group.blocks.at(blockIndex)?.outgoingEdgeId
|
||||
if (!outgoingEdgeId) return
|
||||
const outgoingEdge = typebot.edges.find(byId(outgoingEdgeId))
|
||||
if (!outgoingEdge) return
|
||||
const connectedGroup = typebot.groups.find(byId(outgoingEdge?.to.groupId))
|
||||
if (!connectedGroup) return
|
||||
return outgoingEdge.to.blockId
|
||||
? connectedGroup.blocks.find(
|
||||
(block) => block.id === outgoingEdge.to.blockId
|
||||
)
|
||||
: connectedGroup?.blocks.at(0)
|
||||
}
|
||||
|
||||
const isCredentialsV2 = (credentials: { iv: string }) =>
|
||||
credentials.iv.length === 24
|
@ -16,6 +16,7 @@
|
||||
"@typebot.io/prisma": "workspace:*",
|
||||
"@typebot.io/schemas": "workspace:*",
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*",
|
||||
"@udecode/plate-common": "21.1.5",
|
||||
"@udecode/plate-serializer-md": "24.4.0",
|
||||
"ai": "2.2.24",
|
||||
@ -34,6 +35,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "6.4.8",
|
||||
"@types/qs": "6.9.7"
|
||||
"@types/qs": "6.9.7",
|
||||
"@typebot.io/forge-schemas": "workspace:*",
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
"@typebot.io/forge-repository": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
ContinueChatResponse,
|
||||
Typebot,
|
||||
} from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from './variables/deepParseVariables'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import {
|
||||
getVariablesToParseInfoInText,
|
||||
parseVariables,
|
||||
} from './variables/parseVariables'
|
||||
} from '@typebot.io/variables/parseVariables'
|
||||
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
|
||||
import {
|
||||
createDeserializeMdPlugin,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user