2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View File

@ -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 [
{

View File

@ -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:*",

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,3 @@
.md-link {
text-decoration: underline;
}

View File

@ -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>
)
}

View File

@ -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={{

View File

@ -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>
)
}

View File

@ -160,7 +160,7 @@ export const TextInput = forwardRef(function TextInput(
) : (
Input
)}
{helperText && <FormHelperText>{helperText}</FormHelperText>}
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
</FormControl>
)
})

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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} />
}
}

View File

@ -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} />
}
}

View File

@ -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>

View 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>
)
}

View 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 }} />
}

View 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>
}

View File

@ -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 }
})

View File

@ -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 }
}
)

View File

@ -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 }
})

View 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,
})

View 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,
}),
}
}
)

View File

@ -0,0 +1,6 @@
import { router } from '@/helpers/server/trpc'
import { fetchSelectItems } from './fetchSelectItems'
export const integrationsRouter = router({
fetchSelectItems,
})

View 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
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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}
/>
)}
</>
)
}

View File

@ -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}
/>
)}
</>
)
}

View File

@ -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>
)

View File

@ -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]
}, [])

View 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])

View File

@ -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}

View File

@ -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} />
}
}
}

View File

@ -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>
)
}

View File

@ -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} />
)
}
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>
)}

View File

@ -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
),
},
}
}
}

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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",

View File

@ -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"
},

View File

@ -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 [
{

View File

@ -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",

View File

@ -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' },

View File

@ -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())

View File

@ -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'

View File

@ -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",

View File

@ -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])

View File

@ -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) => {

View File

@ -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,

View File

@ -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'

View File

@ -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[]) =>

View File

@ -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 = (

View File

@ -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,

View File

@ -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[]) =>

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 (

View File

@ -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'

View File

@ -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,
])

View File

@ -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 =
(

View File

@ -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,

View File

@ -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 (

View File

@ -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,

View File

@ -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

View File

@ -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'

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 = (

View File

@ -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 = (

View File

@ -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

View File

@ -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,

View File

@ -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)
}
}

View 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

View File

@ -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:*"
}
}

View File

@ -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