diff --git a/apps/builder/next.config.mjs b/apps/builder/next.config.mjs index a2f944291..e3414961e 100644 --- a/apps/builder/next.config.mjs +++ b/apps/builder/next.config.mjs @@ -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 [ { diff --git a/apps/builder/package.json b/apps/builder/package.json index 2a929d1bf..59ee08b57 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -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:*", diff --git a/apps/builder/public/templates/audio-chat-gpt.json b/apps/builder/public/templates/audio-chat-gpt.json index 189b91a8e..3cf83c687 100644 --- a/apps/builder/public/templates/audio-chat-gpt.json +++ b/apps/builder/public/templates/audio-chat-gpt.json @@ -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, diff --git a/apps/builder/public/templates/basic-chat-gpt.json b/apps/builder/public/templates/basic-chat-gpt.json index dbd8a4a1f..4d3af96bc 100644 --- a/apps/builder/public/templates/basic-chat-gpt.json +++ b/apps/builder/public/templates/basic-chat-gpt.json @@ -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, diff --git a/apps/builder/public/templates/chat-gpt-personas.json b/apps/builder/public/templates/chat-gpt-personas.json index fbc900556..995a76e36 100644 --- a/apps/builder/public/templates/chat-gpt-personas.json +++ b/apps/builder/public/templates/chat-gpt-personas.json @@ -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, diff --git a/apps/builder/public/templates/openai-conditions.json b/apps/builder/public/templates/openai-conditions.json index 6fe7fc374..283516f5f 100644 --- a/apps/builder/public/templates/openai-conditions.json +++ b/apps/builder/public/templates/openai-conditions.json @@ -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, diff --git a/apps/builder/src/assets/styles/md.css b/apps/builder/src/assets/styles/md.css new file mode 100644 index 000000000..83e09cefc --- /dev/null +++ b/apps/builder/src/assets/styles/md.css @@ -0,0 +1,3 @@ +.md-link { + text-decoration: underline; +} diff --git a/apps/builder/src/components/DropdownList.tsx b/apps/builder/src/components/DropdownList.tsx index 45d33fdfc..b878bd775 100644 --- a/apps/builder/src/components/DropdownList.tsx +++ b/apps/builder/src/components/DropdownList.tsx @@ -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 = { @@ -18,6 +23,11 @@ type Props = { 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 = ({ currentItem, onItemSelect, items, - placeholder = '', + placeholder, + label, + isRequired, + direction = 'column', + helperText, + moreInfoTooltip, ...props }: Props & ButtonProps) => { const handleMenuItemClick = (operator: T[number]) => () => { onItemSelect(operator) } return ( - - } - colorScheme="gray" - justifyContent="space-between" - textAlign="left" - {...props} - > - - {currentItem ?? placeholder} - - - - - - {items.map((item) => ( - - {item} - - ))} - - - - + + {label && ( + + {label}{' '} + {moreInfoTooltip && ( + {moreInfoTooltip} + )} + + )} + + } + colorScheme="gray" + justifyContent="space-between" + textAlign="left" + w="full" + {...props} + > + + {currentItem ?? placeholder ?? 'Select an item'} + + + + + + {items.map((item) => ( + + {item} + + ))} + + + + + {helperText && {helperText}} + ) } diff --git a/apps/builder/src/components/TableList.tsx b/apps/builder/src/components/TableList.tsx index 34f9a734c..89d2ef55b 100644 --- a/apps/builder/src/components/TableList.tsx +++ b/apps/builder/src/components/TableList.tsx @@ -28,9 +28,9 @@ type Props = { addLabel?: string newItemDefaultProps?: Partial hasDefaultItem?: boolean - Item: (props: TableListItemProps) => JSX.Element ComponentBetweenItems?: (props: unknown) => JSX.Element onItemsChange: (items: ItemWithId[]) => void + children: (props: TableListItemProps) => JSX.Element } export const TableList = ({ @@ -39,7 +39,7 @@ export const TableList = ({ addLabel = 'Add', newItemDefaultProps, hasDefaultItem, - Item, + children, ComponentBetweenItems, onItemsChange, }: Props) => { @@ -107,7 +107,7 @@ export const TableList = ({ justifyContent="center" pb="4" > - + {children({ item, onItemChange: handleCellChange(itemIndex) })} = { isRequired?: boolean direction?: 'row' | 'column' suffix?: string + helperText?: ReactNode onValueChange: (value?: Value) => void } & Omit @@ -42,8 +44,9 @@ export const NumberInput = ({ label, moreInfoTooltip, isRequired, - direction, + direction = 'column', suffix, + helperText, ...props }: Props) => { const [value, setValue] = useState(defaultValue?.toString() ?? '') @@ -87,7 +90,12 @@ export const NumberInput = ({ } const Input = ( - + @@ -105,16 +113,16 @@ export const NumberInput = ({ spacing={direction === 'column' ? 2 : 3} > {label && ( - + {label}{' '} {moreInfoTooltip && ( {moreInfoTooltip} )} )} - + {withVariableButton ?? true ? ( - + {Input} @@ -123,6 +131,7 @@ export const NumberInput = ({ )} {suffix ? {suffix} : null} + {helperText ? {helperText} : null} ) } diff --git a/apps/builder/src/components/inputs/TextInput.tsx b/apps/builder/src/components/inputs/TextInput.tsx index 7a23e876f..3c2a40493 100644 --- a/apps/builder/src/components/inputs/TextInput.tsx +++ b/apps/builder/src/components/inputs/TextInput.tsx @@ -160,7 +160,7 @@ export const TextInput = forwardRef(function TextInput( ) : ( Input )} - {helperText && {helperText}} + {helperText && {helperText}} ) }) diff --git a/apps/builder/src/components/inputs/Textarea.tsx b/apps/builder/src/components/inputs/Textarea.tsx index 59f620b9a..cb8783140 100644 --- a/apps/builder/src/components/inputs/Textarea.tsx +++ b/apps/builder/src/components/inputs/Textarea.tsx @@ -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 export const Textarea = ({ @@ -37,6 +41,8 @@ export const Textarea = ({ withVariableButton = true, isRequired, minH, + helperText, + direction = 'column', }: Props) => { const inputRef = useRef(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 ( - + {label && ( - + {label}{' '} {moreInfoTooltip && ( {moreInfoTooltip} @@ -115,6 +127,7 @@ export const Textarea = ({ ) : ( Textarea )} + {helperText && {helperText}} ) } diff --git a/apps/builder/src/components/inputs/VariableSearchInput.tsx b/apps/builder/src/components/inputs/VariableSearchInput.tsx index b31d055f2..607114533 100644 --- a/apps/builder/src/components/inputs/VariableSearchInput.tsx +++ b/apps/builder/src/components/inputs/VariableSearchInput.tsx @@ -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 | undefined ) => void -} & InputProps + label?: string + placeholder?: string + helperText?: ReactNode + moreInfoTooltip?: string + direction?: 'row' | 'column' +} & Omit 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 ( - - - - - - - e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - > - {isCreateVariableButtonDisplayed && ( - - )} - {filteredItems.length > 0 && ( - <> - {filteredItems.map((item, idx) => { - const indexInList = isCreateVariableButtonDisplayed - ? idx + 1 - : idx - return ( - + )} + {filteredItems.length > 0 && ( + <> + {filteredItems.map((item, idx) => { + const indexInList = isCreateVariableButtonDisplayed + ? idx + 1 + : idx + return ( + - ) - })} - - )} - - - - + + } + aria-label="Rename variable" + size="xs" + onClick={handleRenameVariableClick(item)} + /> + } + aria-label="Remove variable" + size="xs" + onClick={handleDeleteVariableClick(item)} + /> + + + ) + })} + + )} + + + + + {helperText && {helperText}} + ) } diff --git a/apps/builder/src/features/blocks/inputs/buttons/buttons.spec.ts b/apps/builder/src/features/blocks/inputs/buttons/buttons.spec.ts index 6cc0966cd..e675aca51 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/buttons.spec.ts +++ b/apps/builder/src/features/blocks/inputs/buttons/buttons.spec.ts @@ -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 diff --git a/apps/builder/src/features/blocks/inputs/pictureChoice/pictureChoice.spec.ts b/apps/builder/src/features/blocks/inputs/pictureChoice/pictureChoice.spec.ts index 73c112b9b..f89f986d5 100644 --- a/apps/builder/src/features/blocks/inputs/pictureChoice/pictureChoice.spec.ts +++ b/apps/builder/src/features/blocks/inputs/pictureChoice/pictureChoice.spec.ts @@ -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() diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx index 7953b8ede..8b8cd8358 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx @@ -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) { - return - }, - [sheet?.columns] - ) - - const ExtractingCellItem = useMemo( - () => - function Component(props: TableListItemProps) { - return ( - - ) - }, - [sheet?.columns] - ) - switch (options.action) { case GoogleSheetsAction.INSERT_ROW: return ( initialItems={options.cellsToInsert} onItemsChange={handleInsertColumnsChange} - Item={UpdatingCellItem} addLabel="Add a value" - /> + > + {({ item, onItemChange }) => ( + + )} + ) case GoogleSheetsAction.UPDATE_ROW: return ( @@ -236,9 +225,16 @@ const ActionOptions = ({ initialItems={options.cellsToUpsert} onItemsChange={handleUpsertColumnsChange} - Item={UpdatingCellItem} addLabel="Add a value" - /> + > + {({ item, onItemChange }) => ( + + )} + @@ -286,10 +282,17 @@ const ActionOptions = ({ initialItems={options.cellsToExtract} onItemsChange={handleExtractingCellsChange} - Item={ExtractingCellItem} addLabel="Add a value" hasDefaultItem - /> + > + {({ item, onItemChange }) => ( + + )} + diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/components/RowsFilterTableList.tsx b/apps/builder/src/features/blocks/integrations/googleSheets/components/RowsFilterTableList.tsx index 904b70fad..c6b2e7412 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/components/RowsFilterTableList.tsx +++ b/apps/builder/src/features/blocks/integrations/googleSheets/components/RowsFilterTableList.tsx @@ -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) => ( - - ), - [columns] - ) - return ( initialItems={filter?.comparisons ?? []} onItemsChange={updateComparisons} - Item={createRowsFilterComparisonItem} ComponentBetweenItems={() => ( )} addLabel="Add filter rule" - /> + > + {(props) => } + ) } diff --git a/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/OpenAIChatCompletionSettings.tsx b/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/OpenAIChatCompletionSettings.tsx index 03eaf121d..17197dbe6 100644 --- a/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/OpenAIChatCompletionSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/OpenAIChatCompletionSettings.tsx @@ -94,12 +94,13 @@ export const OpenAIChatCompletionSettings = ({ + > + {(props) => } + @@ -131,11 +132,12 @@ export const OpenAIChatCompletionSettings = ({ + > + {(props) => } + diff --git a/apps/builder/src/features/blocks/integrations/pixel/components/PixelSettings.tsx b/apps/builder/src/features/blocks/integrations/pixel/components/PixelSettings.tsx index d2899d628..f311a1850 100644 --- a/apps/builder/src/features/blocks/integrations/pixel/components/PixelSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/pixel/components/PixelSettings.tsx @@ -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) { - return - }, - [options?.eventType] - ) - return ( { ).length > 0) && ( + > + {(props) => ( + + )} + )} diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx index 62709b48a..0d0e5e173 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx +++ b/apps/builder/src/features/blocks/integrations/webhook/components/WebhookAdvancedConfigForm.tsx @@ -154,9 +154,10 @@ export const WebhookAdvancedConfigForm = ({ initialItems={webhook?.queryParams} onItemsChange={updateQueryParams} - Item={QueryParamsInputs} addLabel="Add a param" - /> + > + {(props) => } + @@ -168,9 +169,10 @@ export const WebhookAdvancedConfigForm = ({ initialItems={webhook?.headers} onItemsChange={updateHeaders} - Item={HeadersInputs} addLabel="Add a value" - /> + > + {(props) => } + @@ -203,9 +205,10 @@ export const WebhookAdvancedConfigForm = ({ initialItems={options?.variablesForTest} onItemsChange={updateVariablesForTest} - Item={VariableForTestInputs} addLabel="Add an entry" - /> + > + {(props) => } + @@ -235,9 +238,10 @@ export const WebhookAdvancedConfigForm = ({ initialItems={options?.responseVariableMapping} onItemsChange={updateResponseVariableMapping} - Item={ResponseMappingInputs} addLabel="Add an entry" - /> + > + {(props) => } + diff --git a/apps/builder/src/features/blocks/integrations/zemanticAi/ZemanticAiSettings.tsx b/apps/builder/src/features/blocks/integrations/zemanticAi/ZemanticAiSettings.tsx index 392933a2f..c6442ecfa 100644 --- a/apps/builder/src/features/blocks/integrations/zemanticAi/ZemanticAiSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/zemanticAi/ZemanticAiSettings.tsx @@ -171,10 +171,11 @@ export const ZemanticAiSettings = ({ + > + {(props) => } + diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionForm.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionForm.tsx index 9b266dcd1..03396e352 100644 --- a/apps/builder/src/features/blocks/logic/condition/components/ConditionForm.tsx +++ b/apps/builder/src/features/blocks/logic/condition/components/ConditionForm.tsx @@ -24,7 +24,6 @@ export const ConditionForm = ({ condition, onConditionChange }: Props) => { initialItems={condition?.comparisons} onItemsChange={handleComparisonsChange} - Item={ComparisonItem} ComponentBetweenItems={() => ( { )} addLabel="Add a comparison" - /> + > + {(props) => } + ) } diff --git a/apps/builder/src/features/editor/components/BlockCard.tsx b/apps/builder/src/features/editor/components/BlockCard.tsx index aef62dfba..7f696bb74 100644 --- a/apps/builder/src/features/editor/components/BlockCard.tsx +++ b/apps/builder/src/features/editor/components/BlockCard.tsx @@ -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 ( + + ) + } 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 ( - - - - {!isMouseDown ? children : null} - - - - ) -} diff --git a/apps/builder/src/features/editor/components/BlockCardLayout.tsx b/apps/builder/src/features/editor/components/BlockCardLayout.tsx new file mode 100644 index 000000000..a013d01aa --- /dev/null +++ b/apps/builder/src/features/editor/components/BlockCardLayout.tsx @@ -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 ( + + + + {!isMouseDown ? children : null} + + + + ) +} diff --git a/apps/builder/src/features/editor/components/BlockIcon.tsx b/apps/builder/src/features/editor/components/BlockIcon.tsx index 19e0695c9..7fca0282e 100644 --- a/apps/builder/src/features/editor/components/BlockIcon.tsx +++ b/apps/builder/src/features/editor/components/BlockIcon.tsx @@ -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 + return case BubbleBlockType.IMAGE: - return + return case BubbleBlockType.VIDEO: - return + return case BubbleBlockType.EMBED: - return + return case BubbleBlockType.AUDIO: - return + return case InputBlockType.TEXT: - return + return case InputBlockType.NUMBER: - return + return case InputBlockType.EMAIL: - return + return case InputBlockType.URL: - return + return case InputBlockType.DATE: - return + return case InputBlockType.PHONE: - return + return case InputBlockType.CHOICE: - return + return case InputBlockType.PICTURE_CHOICE: - return + return case InputBlockType.PAYMENT: - return + return case InputBlockType.RATING: - return + return case InputBlockType.FILE: - return + return case LogicBlockType.SET_VARIABLE: - return + return case LogicBlockType.CONDITION: - return + return case LogicBlockType.REDIRECT: - return + return case LogicBlockType.SCRIPT: - return + return case LogicBlockType.WAIT: - return + return case LogicBlockType.JUMP: - return + return case LogicBlockType.TYPEBOT_LINK: - return + return case LogicBlockType.AB_TEST: - return + return case IntegrationBlockType.GOOGLE_SHEETS: - return + return case IntegrationBlockType.GOOGLE_ANALYTICS: - return + return case IntegrationBlockType.WEBHOOK: - return + return case IntegrationBlockType.ZAPIER: - return + return case IntegrationBlockType.MAKE_COM: - return + return case IntegrationBlockType.PABBLY_CONNECT: - return + return case IntegrationBlockType.EMAIL: - return + return case IntegrationBlockType.CHATWOOT: - return - case IntegrationBlockType.OPEN_AI: - return + return case IntegrationBlockType.PIXEL: - return + return case IntegrationBlockType.ZEMANTIC_AI: - return + return case 'start': - return + return + case IntegrationBlockType.OPEN_AI: + return + default: + return } } diff --git a/apps/builder/src/features/editor/components/BlockLabel.tsx b/apps/builder/src/features/editor/components/BlockLabel.tsx index 9e799d858..d724298ac 100644 --- a/apps/builder/src/features/editor/components/BlockLabel.tsx +++ b/apps/builder/src/features/editor/components/BlockLabel.tsx @@ -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 ( {t('editor.sidebarBlock.zemanticAi.label')} ) + default: + return } } diff --git a/apps/builder/src/features/editor/components/BlocksSideBar.tsx b/apps/builder/src/features/editor/components/BlocksSideBar.tsx index 614e02eeb..396d9fcc2 100644 --- a/apps/builder/src/features/editor/components/BlocksSideBar.tsx +++ b/apps/builder/src/features/editor/components/BlocksSideBar.tsx @@ -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')} - {Object.values(IntegrationBlockType).map((type) => ( - - ))} + {Object.values(IntegrationBlockType) + .concat(enabledBlocks as any) + .filter((type) => !legacyIntegrationBlocks.includes(type)) + .map((type) => ( + + ))} diff --git a/apps/builder/src/features/forge/ForgedBlockCard.tsx b/apps/builder/src/features/forge/ForgedBlockCard.tsx new file mode 100644 index 000000000..2821cb236 --- /dev/null +++ b/apps/builder/src/features/forge/ForgedBlockCard.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/builder/src/features/forge/ForgedBlockIcon.tsx b/apps/builder/src/features/forge/ForgedBlockIcon.tsx new file mode 100644 index 000000000..fc2faf911 --- /dev/null +++ b/apps/builder/src/features/forge/ForgedBlockIcon.tsx @@ -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 + return +} diff --git a/apps/builder/src/features/forge/ForgedBlockLabel.tsx b/apps/builder/src/features/forge/ForgedBlockLabel.tsx new file mode 100644 index 000000000..0b079eda5 --- /dev/null +++ b/apps/builder/src/features/forge/ForgedBlockLabel.tsx @@ -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 {blockDef?.name} +} diff --git a/apps/builder/src/features/forge/api/credentials/createCredentials.ts b/apps/builder/src/features/forge/api/credentials/createCredentials.ts new file mode 100644 index 000000000..5957144ce --- /dev/null +++ b/apps/builder/src/features/forge/api/credentials/createCredentials.ts @@ -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 } + }) diff --git a/apps/builder/src/features/forge/api/credentials/deleteCredentials.ts b/apps/builder/src/features/forge/api/credentials/deleteCredentials.ts new file mode 100644 index 000000000..9929607ff --- /dev/null +++ b/apps/builder/src/features/forge/api/credentials/deleteCredentials.ts @@ -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 } + } + ) diff --git a/apps/builder/src/features/forge/api/credentials/listCredentials.ts b/apps/builder/src/features/forge/api/credentials/listCredentials.ts new file mode 100644 index 000000000..d588561a4 --- /dev/null +++ b/apps/builder/src/features/forge/api/credentials/listCredentials.ts @@ -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 } + }) diff --git a/apps/builder/src/features/forge/api/credentials/router.ts b/apps/builder/src/features/forge/api/credentials/router.ts new file mode 100644 index 000000000..f22addf17 --- /dev/null +++ b/apps/builder/src/features/forge/api/credentials/router.ts @@ -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, +}) diff --git a/apps/builder/src/features/forge/api/fetchSelectItems.ts b/apps/builder/src/features/forge/api/fetchSelectItems.ts new file mode 100644 index 000000000..659f916e6 --- /dev/null +++ b/apps/builder/src/features/forge/api/fetchSelectItems.ts @@ -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, + }), + } + } + ) diff --git a/apps/builder/src/features/forge/api/router.ts b/apps/builder/src/features/forge/api/router.ts new file mode 100644 index 000000000..e6f7637c6 --- /dev/null +++ b/apps/builder/src/features/forge/api/router.ts @@ -0,0 +1,6 @@ +import { router } from '@/helpers/server/trpc' +import { fetchSelectItems } from './fetchSelectItems' + +export const integrationsRouter = router({ + fetchSelectItems, +}) diff --git a/apps/builder/src/features/forge/components/ForgeSelectInput.tsx b/apps/builder/src/features/forge/components/ForgeSelectInput.tsx new file mode 100644 index 000000000..2d9dfa671 --- /dev/null +++ b/apps/builder/src/features/forge/components/ForgeSelectInput.tsx @@ -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 ( + + {label && ( + + {label}{' '} + {moreInfoTooltip && ( + {moreInfoTooltip} + )} + + )} +