diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index c689a56cf..dfd4bf379 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -33,9 +33,3104 @@ "startParams": { "type": "object", "properties": { - "typebotId": { - "type": "string", - "description": "[How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)" + "typebot": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "graphCoordinates": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "additionalProperties": false + }, + "blocks": { + "type": "array", + "items": { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "start" + ] + }, + "label": { + "type": "string" + } + }, + "required": [ + "type", + "label" + ], + "additionalProperties": false + } + ] + }, + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "content": { + "type": "object", + "properties": { + "html": { + "type": "string" + }, + "richText": { + "type": "array" + }, + "plainText": { + "type": "string" + } + }, + "required": [ + "html", + "richText", + "plainText" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "image" + ] + }, + "content": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "video" + ] + }, + "content": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "url", + "youtube", + "vimeo" + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "embed" + ] + }, + "content": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "height": { + "type": "number" + } + }, + "required": [ + "height" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "audio" + ] + }, + "content": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + } + ] + } + ] + }, + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text input" + ] + }, + "options": { + "allOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + } + }, + "required": [ + "labels" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + { + "type": "object", + "properties": { + "isLong": { + "type": "boolean" + } + }, + "required": [ + "isLong" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "number input" + ] + }, + "options": { + "allOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + } + }, + "required": [ + "labels" + ], + "additionalProperties": false + } + ] + }, + { + "type": "object", + "properties": { + "min": { + "type": "number" + }, + "max": { + "type": "number" + }, + "step": { + "type": "number" + } + }, + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "email input" + ] + }, + "options": { + "allOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + } + }, + "required": [ + "labels" + ], + "additionalProperties": false + } + ] + }, + { + "type": "object", + "properties": { + "retryMessageContent": { + "type": "string" + } + }, + "required": [ + "retryMessageContent" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "url input" + ] + }, + "options": { + "allOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + } + }, + "required": [ + "labels" + ], + "additionalProperties": false + } + ] + }, + { + "type": "object", + "properties": { + "retryMessageContent": { + "type": "string" + } + }, + "required": [ + "retryMessageContent" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "date input" + ] + }, + "options": { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "button": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + }, + "required": [ + "button", + "from", + "to" + ], + "additionalProperties": false + }, + "hasTime": { + "type": "boolean" + }, + "isRange": { + "type": "boolean" + } + }, + "required": [ + "labels", + "hasTime", + "isRange" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone number input" + ] + }, + "options": { + "allOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + } + }, + "required": [ + "labels" + ], + "additionalProperties": false + } + ] + }, + { + "type": "object", + "properties": { + "retryMessageContent": { + "type": "string" + }, + "defaultCountryCode": { + "type": "string" + } + }, + "required": [ + "retryMessageContent" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "choice input" + ] + }, + "items": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "blockId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "number", + "enum": [ + 0 + ] + }, + "content": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + ] + } + }, + "options": { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "isMultipleChoice": { + "type": "boolean" + }, + "buttonLabel": { + "type": "string" + } + }, + "required": [ + "isMultipleChoice", + "buttonLabel" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "items", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "payment input" + ] + }, + "options": { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "Stripe" + ] + }, + "labels": { + "type": "object", + "properties": { + "button": { + "type": "string" + }, + "success": { + "type": "string" + } + }, + "required": [ + "button" + ], + "additionalProperties": false + }, + "additionalInformation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + } + }, + "additionalProperties": false + }, + "credentialsId": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "amount": { + "type": "string" + } + }, + "required": [ + "provider", + "labels", + "currency" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "rating input" + ] + }, + "options": { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "buttonType": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Icons" + ] + }, + { + "type": "string", + "enum": [ + "Numbers" + ] + } + ] + }, + "length": { + "type": "number" + }, + "labels": { + "type": "object", + "properties": { + "left": { + "type": "string" + }, + "right": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "button" + ], + "additionalProperties": false + }, + "customIcon": { + "type": "object", + "properties": { + "isEnabled": { + "type": "boolean" + }, + "svg": { + "type": "string" + } + }, + "required": [ + "isEnabled" + ], + "additionalProperties": false + }, + "isOneClickSubmitEnabled": { + "type": "boolean" + } + }, + "required": [ + "buttonType", + "length", + "labels", + "customIcon" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file input" + ] + }, + "options": { + "allOf": [ + { + "type": "object", + "properties": { + "variableId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "isRequired": { + "type": "boolean" + }, + "isMultipleAllowed": { + "type": "boolean" + }, + "labels": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + }, + "button": { + "type": "string" + } + }, + "required": [ + "placeholder", + "button" + ], + "additionalProperties": false + }, + "sizeLimit": { + "type": "number" + } + }, + "required": [ + "isMultipleAllowed", + "labels" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + } + ] + }, + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Code" + ] + }, + "options": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + }, + "shouldExecuteInParentContext": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Condition" + ] + }, + "items": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "blockId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "number", + "enum": [ + 1 + ] + }, + "content": { + "type": "object", + "properties": { + "logicalOperator": { + "type": "string", + "enum": [ + "OR", + "AND" + ] + }, + "comparisons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "comparisonOperator": { + "type": "string", + "enum": [ + "Equal to", + "Not equal", + "Contains", + "Greater than", + "Less than", + "Is set" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "logicalOperator", + "comparisons" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "content" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "type", + "items" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Redirect" + ] + }, + "options": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "isNewTab": { + "type": "boolean" + } + }, + "required": [ + "isNewTab" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Typebot link" + ] + }, + "options": { + "type": "object", + "properties": { + "typebotId": { + "type": "string" + }, + "groupId": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Set variable" + ] + }, + "options": { + "type": "object", + "properties": { + "variableId": { + "type": "string" + }, + "expressionToEvaluate": { + "type": "string" + }, + "isCode": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + } + ] + }, + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Google Sheets" + ] + }, + "options": { + "anyOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "credentialsId": { + "type": "string" + }, + "sheetId": { + "type": "string" + }, + "spreadsheetId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "credentialsId": { + "type": "string" + }, + "sheetId": { + "type": "string" + }, + "spreadsheetId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "Get data from sheet" + ] + }, + "referenceCell": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "value": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "filter": { + "type": "object", + "properties": { + "comparisons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "column": { + "type": "string" + }, + "comparisonOperator": { + "type": "string", + "enum": [ + "Equal to", + "Not equal", + "Contains", + "Greater than", + "Less than", + "Is set" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "logicalOperator": { + "type": "string", + "enum": [ + "OR", + "AND" + ] + } + }, + "required": [ + "comparisons", + "logicalOperator" + ], + "additionalProperties": false + }, + "cellsToExtract": { + "type": "array", + "items": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "action", + "filter", + "cellsToExtract" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "credentialsId": { + "type": "string" + }, + "sheetId": { + "type": "string" + }, + "spreadsheetId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "Insert a row" + ] + }, + "cellsToInsert": { + "type": "array", + "items": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "value": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "action", + "cellsToInsert" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "credentialsId": { + "type": "string" + }, + "sheetId": { + "type": "string" + }, + "spreadsheetId": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "Update a row" + ] + }, + "cellsToUpsert": { + "type": "array", + "items": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "value": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "referenceCell": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "value": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "action", + "cellsToUpsert" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Google Analytics" + ] + }, + "options": { + "type": "object", + "properties": { + "trackingId": { + "type": "string" + }, + "category": { + "type": "string" + }, + "action": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Webhook" + ] + }, + "options": { + "type": "object", + "properties": { + "variablesForTest": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "isAdvancedConfig": { + "type": "boolean" + }, + "isCustomBody": { + "type": "boolean" + } + }, + "required": [ + "variablesForTest", + "responseVariableMapping" + ], + "additionalProperties": false + }, + "webhookId": { + "type": "string" + } + }, + "required": [ + "type", + "options", + "webhookId" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Email" + ] + }, + "options": { + "type": "object", + "properties": { + "credentialsId": { + "type": "string" + }, + "isCustomBody": { + "type": "boolean" + }, + "isBodyCode": { + "type": "boolean" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + } + }, + "subject": { + "type": "string" + }, + "body": { + "type": "string" + }, + "replyTo": { + "type": "string" + }, + "cc": { + "type": "array", + "items": { + "type": "string" + } + }, + "bcc": { + "type": "array", + "items": { + "type": "string" + } + }, + "attachmentsVariableId": { + "type": "string" + } + }, + "required": [ + "credentialsId", + "recipients" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Zapier" + ] + }, + "options": { + "type": "object", + "properties": { + "variablesForTest": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "isAdvancedConfig": { + "type": "boolean" + }, + "isCustomBody": { + "type": "boolean" + } + }, + "required": [ + "variablesForTest", + "responseVariableMapping" + ], + "additionalProperties": false + }, + "webhookId": { + "type": "string" + } + }, + "required": [ + "type", + "options", + "webhookId" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Make.com" + ] + }, + "options": { + "type": "object", + "properties": { + "variablesForTest": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "isAdvancedConfig": { + "type": "boolean" + }, + "isCustomBody": { + "type": "boolean" + } + }, + "required": [ + "variablesForTest", + "responseVariableMapping" + ], + "additionalProperties": false + }, + "webhookId": { + "type": "string" + } + }, + "required": [ + "type", + "options", + "webhookId" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Pabbly" + ] + }, + "options": { + "type": "object", + "properties": { + "variablesForTest": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "isAdvancedConfig": { + "type": "boolean" + }, + "isCustomBody": { + "type": "boolean" + } + }, + "required": [ + "variablesForTest", + "responseVariableMapping" + ], + "additionalProperties": false + }, + "webhookId": { + "type": "string" + } + }, + "required": [ + "type", + "options", + "webhookId" + ], + "additionalProperties": false + } + ] + } + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + } + }, + "required": [ + "id", + "groupId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Chatwoot" + ] + }, + "options": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string" + }, + "websiteToken": { + "type": "string" + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "baseUrl", + "websiteToken" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "options" + ], + "additionalProperties": false + } + ] + } + ] + } + ] + } + } + }, + "required": [ + "id", + "title", + "graphCoordinates", + "blocks" + ], + "additionalProperties": false + } + }, + "edges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "from": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "itemId": { + "type": "string" + } + }, + "required": [ + "groupId", + "blockId" + ], + "additionalProperties": false + }, + "to": { + "type": "object", + "properties": { + "groupId": { + "type": "string" + }, + "blockId": { + "type": "string" + } + }, + "required": [ + "groupId" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "from", + "to" + ], + "additionalProperties": false + } + }, + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "not": {} + }, + { + "type": "string" + } + ], + "nullable": true + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + } + }, + "settings": { + "type": "object", + "properties": { + "general": { + "type": "object", + "properties": { + "isBrandingEnabled": { + "type": "boolean" + }, + "isTypingEmulationEnabled": { + "type": "boolean" + }, + "isInputPrefillEnabled": { + "type": "boolean" + }, + "isHideQueryParamsEnabled": { + "type": "boolean" + }, + "isNewResultOnRefreshEnabled": { + "type": "boolean" + }, + "isResultSavingEnabled": { + "type": "boolean" + } + }, + "required": [ + "isBrandingEnabled" + ], + "additionalProperties": false + }, + "typingEmulation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "speed": { + "type": "number" + }, + "maxDelay": { + "type": "number" + } + }, + "required": [ + "enabled", + "speed", + "maxDelay" + ], + "additionalProperties": false + }, + "metadata": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "imageUrl": { + "type": "string" + }, + "favIconUrl": { + "type": "string" + }, + "customHeadCode": { + "type": "string" + }, + "googleTagManagerId": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "general", + "typingEmulation", + "metadata" + ], + "additionalProperties": false + }, + "theme": { + "type": "object", + "properties": { + "general": { + "type": "object", + "properties": { + "font": { + "type": "string" + }, + "background": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Color", + "Image", + "None" + ] + }, + "content": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "font", + "background" + ], + "additionalProperties": false + }, + "chat": { + "type": "object", + "properties": { + "hostAvatar": { + "type": "object", + "properties": { + "isEnabled": { + "type": "boolean" + }, + "url": { + "type": "string" + } + }, + "required": [ + "isEnabled" + ], + "additionalProperties": false + }, + "guestAvatar": { + "type": "object", + "properties": { + "isEnabled": { + "type": "boolean" + }, + "url": { + "type": "string" + } + }, + "required": [ + "isEnabled" + ], + "additionalProperties": false + }, + "hostBubbles": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "backgroundColor", + "color" + ], + "additionalProperties": false + }, + "guestBubbles": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "backgroundColor", + "color" + ], + "additionalProperties": false + }, + "buttons": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "backgroundColor", + "color" + ], + "additionalProperties": false + }, + "inputs": { + "allOf": [ + { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "backgroundColor", + "color" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "placeholderColor": { + "type": "string" + } + }, + "required": [ + "placeholderColor" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "hostBubbles", + "guestBubbles", + "buttons", + "inputs" + ], + "additionalProperties": false + }, + "customCss": { + "type": "string" + } + }, + "required": [ + "general", + "chat" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "groups", + "edges", + "variables", + "settings", + "theme" + ], + "additionalProperties": false + }, + { + "type": "string" + } + ], + "description": "Either a Typebot ID or a Typebot object. If you provide a Typebot object, it will be executed in preview mode. ([How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid))." }, "isPreview": { "type": "boolean", @@ -45,13 +3140,17 @@ "type": "string", "description": "Provide it if you'd like to overwrite an existing result." }, + "startGroupId": { + "type": "string", + "description": "Start chat from a specific group." + }, "prefilledVariables": { "type": "object", "additionalProperties": {} } }, "required": [ - "typebotId" + "typebot" ], "additionalProperties": false } @@ -1463,6 +4562,9 @@ "typebot": { "type": "object", "properties": { + "id": { + "type": "string" + }, "theme": { "type": "object", "properties": { @@ -1715,6 +4817,7 @@ } }, "required": [ + "id", "theme", "settings" ], @@ -1722,6 +4825,18 @@ }, "resultId": { "type": "string" + }, + "dynamicTheme": { + "type": "object", + "properties": { + "hostAvatarUrl": { + "type": "string" + }, + "guestAvatarUrl": { + "type": "string" + } + }, + "additionalProperties": false } }, "required": [ diff --git a/apps/docs/openapi/introduction.md b/apps/docs/openapi/introduction.md index 4c9bc3a8b..2b8678ded 100644 --- a/apps/docs/openapi/introduction.md +++ b/apps/docs/openapi/introduction.md @@ -21,8 +21,18 @@ The Chat API allows you to execute (chat) with a typebot. ### How to find my `typebotId` +If you'd like to execute the typebot in preview mode, you will need to provide the ID of the building typebot available in the editor URL: + Get typebot ID + +For published typebot execution, you need to provide the public typebot ID available here: + +Get typebot ID diff --git a/apps/docs/static/img/api/publicId.png b/apps/docs/static/img/api/publicId.png new file mode 100644 index 000000000..85c4a8f83 Binary files /dev/null and b/apps/docs/static/img/api/publicId.png differ diff --git a/apps/viewer/src/components/TypebotPageV2.tsx b/apps/viewer/src/components/TypebotPageV2.tsx index 23db2e433..8887c31d8 100644 --- a/apps/viewer/src/components/TypebotPageV2.tsx +++ b/apps/viewer/src/components/TypebotPageV2.tsx @@ -1,33 +1,22 @@ -import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery' -import { - getExistingResultFromSession, - setResultInSession, -} from '@/utils/sessionStorage' import { Standard } from '@typebot.io/react' -import { BackgroundType, InitialChatReply, Typebot } from 'models' +import { BackgroundType, Typebot } from 'models' import { useRouter } from 'next/router' -import { useCallback, useEffect, useState } from 'react' -import { ErrorPage } from './ErrorPage' import { SEO } from './Seo' export type TypebotPageV2Props = { url: string typebot: Pick< Typebot, - 'settings' | 'theme' | 'id' | 'name' | 'isClosed' | 'isArchived' + 'settings' | 'theme' | 'name' | 'isClosed' | 'isArchived' | 'publicId' > } -let hasInitializedChat = false - export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => { const { asPath, push } = useRouter() - const [initialChatReply, setInitialChatReply] = useState() - const [error, setError] = useState(undefined) const background = typebot.theme.general.background - const clearQueryParamsIfNecessary = useCallback(() => { + const clearQueryParamsIfNecessary = () => { const hasQueryParams = asPath.includes('?') if ( !hasQueryParams || @@ -35,44 +24,8 @@ export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => { ) return push(asPath.split('?')[0], undefined, { shallow: true }) - }, [asPath, push, typebot.settings.general.isHideQueryParamsEnabled]) - - useEffect(() => { - console.log(open) - clearQueryParamsIfNecessary() - }, [clearQueryParamsIfNecessary]) - - useEffect(() => { - if (hasInitializedChat) return - hasInitializedChat = true - const prefilledVariables = extractPrefilledVariables() - const existingResultId = getExistingResultFromSession() ?? undefined - - getInitialChatReplyQuery({ - typebotId: typebot.id, - resultId: - typebot.settings.general.isNewResultOnRefreshEnabled ?? false - ? undefined - : existingResultId, - prefilledVariables, - }).then(({ data, error }) => { - if (error && 'code' in error && error.code === 'FORBIDDEN') { - setError(new Error('This bot is now closed.')) - return - } - if (!data) return setError(new Error("Couldn't initiate the chat")) - setInitialChatReply(data) - setResultInSession(data.resultId) - }) - }, [ - initialChatReply, - typebot.id, - typebot.settings.general.isNewResultOnRefreshEnabled, - ]) - - if (error) { - return } + return (
{ typebotName={typebot.name} metadata={typebot.settings.metadata} /> - {initialChatReply && ( - + {typebot.publicId && ( + )}
) } - -const extractPrefilledVariables = () => { - const urlParams = new URLSearchParams(location.search) - - const prefilledVariables: { [key: string]: string } = {} - urlParams.forEach((value, key) => { - prefilledVariables[key] = value - }) - - return prefilledVariables -} diff --git a/apps/viewer/src/features/blocks/inputs/buttons/api/utils/validateButtonInput.ts b/apps/viewer/src/features/blocks/inputs/buttons/api/utils/validateButtonInput.ts index c66e3e98c..463f2ac06 100644 --- a/apps/viewer/src/features/blocks/inputs/buttons/api/utils/validateButtonInput.ts +++ b/apps/viewer/src/features/blocks/inputs/buttons/api/utils/validateButtonInput.ts @@ -3,4 +3,9 @@ import { ChoiceInputBlock } from 'models' export const validateButtonInput = ( buttonBlock: ChoiceInputBlock, input: string -) => buttonBlock.items.some((item) => item.content === input) +) => + input + .split(',') + .every((value) => + buttonBlock.items.some((item) => item.content === value.trim()) + ) diff --git a/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts b/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts index b30aa351d..bf99830d7 100644 --- a/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts +++ b/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts @@ -2,6 +2,7 @@ import { checkChatsUsage } from '@/features/usage' import { parsePrefilledVariables, deepParseVariable, + parseVariables, } from '@/features/variables' import prisma from '@/lib/prisma' import { publicProcedure } from '@/utils/server/trpc' @@ -11,15 +12,17 @@ import { ChatReply, chatReplySchema, ChatSession, - PublicTypebot, Result, sendMessageInputSchema, SessionState, StartParams, + StartTypebot, + Theme, Typebot, Variable, } from 'models' import { continueBotFlow, getSession, startBotFlow } from '../utils' +import { omit } from 'utils' export const sendMessageProcedure = publicProcedure .meta({ @@ -37,12 +40,13 @@ export const sendMessageProcedure = publicProcedure const session = sessionId ? await getSession(sessionId) : null if (!session) { - const { sessionId, typebot, messages, input, resultId } = + const { sessionId, typebot, messages, input, resultId, dynamicTheme } = await startSession(startParams) return { sessionId, typebot: typebot ? { + id: typebot.id, theme: typebot.theme, settings: typebot.settings, } @@ -50,6 +54,7 @@ export const sendMessageProcedure = publicProcedure messages, input, resultId, + dynamicTheme, } } else { const { messages, input, logic, newSessionState, integrations } = @@ -67,89 +72,35 @@ export const sendMessageProcedure = publicProcedure input, logic, integrations, + dynamicTheme: parseDynamicThemeReply(newSessionState), } } }) const startSession = async (startParams?: StartParams) => { - if (!startParams?.typebotId) + if (!startParams?.typebot) throw new TRPCError({ code: 'BAD_REQUEST', - message: 'No typebotId provided in startParams', - }) - const typebotQuery = startParams.isPreview - ? await prisma.typebot.findUnique({ - where: { id: startParams.typebotId }, - select: { - groups: true, - edges: true, - settings: true, - theme: true, - variables: true, - isArchived: true, - }, - }) - : await prisma.typebot.findUnique({ - where: { id: startParams.typebotId }, - select: { - publishedTypebot: { - select: { - groups: true, - edges: true, - settings: true, - theme: true, - variables: true, - }, - }, - name: true, - isClosed: true, - isArchived: true, - id: true, - }, - }) - - const typebot = - typebotQuery && 'publishedTypebot' in typebotQuery - ? (typebotQuery.publishedTypebot as Pick< - PublicTypebot, - 'groups' | 'edges' | 'settings' | 'theme' | 'variables' - >) - : (typebotQuery as Pick< - Typebot, - 'groups' | 'edges' | 'settings' | 'theme' | 'variables' | 'isArchived' - >) - - if (!typebot) - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Typebot not found', + message: 'No typebot provided in startParams', }) - if ('isClosed' in typebot && typebot.isClosed) - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Typebot is closed', - }) - - const hasReachedLimit = !startParams.isPreview - ? await checkChatsUsage(startParams.typebotId) - : false - - if (hasReachedLimit) - throw new TRPCError({ - code: 'FORBIDDEN', - message: 'Your workspace reached its chat limit', - }) + const typebot = await getTypebot(startParams) const startVariables = startParams.prefilledVariables ? parsePrefilledVariables(typebot.variables, startParams.prefilledVariables) : typebot.variables - const result = await getResult({ ...startParams, startVariables }) + const result = await getResult({ + ...startParams, + typebot: typebot.id, + startVariables, + isNewResultOnRefreshEnabled: + typebot.settings.general.isNewResultOnRefreshEnabled ?? false, + }) const initialState: SessionState = { typebot: { - id: startParams.typebotId, + id: typebot.id, groups: typebot.groups, edges: typebot.edges, variables: startVariables, @@ -161,8 +112,9 @@ const startSession = async (startParams?: StartParams) => { result: result ? { id: result.id, variables: result.variables, hasStarted: false } : undefined, - isPreview: false, - currentTypebotId: startParams.typebotId, + isPreview: startParams.isPreview || typeof startParams.typebot !== 'string', + currentTypebotId: typebot.id, + dynamicTheme: parseDynamicThemeInState(typebot.theme), } const { @@ -170,16 +122,26 @@ const startSession = async (startParams?: StartParams) => { input, logic, newSessionState: newInitialState, - } = await startBotFlow(initialState) + } = await startBotFlow(initialState, startParams.startGroupId) if (!input) return { messages, logic, + typebot: { + id: typebot.id, + settings: deepParseVariable(newInitialState.typebot.variables)( + typebot.settings + ), + theme: deepParseVariable(newInitialState.typebot.variables)( + typebot.theme + ), + }, + dynamicTheme: parseDynamicThemeReply(newInitialState), } const sessionState: ChatSession['state'] = { - ...(newInitialState ?? initialState), + ...newInitialState, currentBlock: { groupId: input.groupId, blockId: input.id, @@ -196,27 +158,122 @@ const startSession = async (startParams?: StartParams) => { resultId: result?.id, sessionId: session.id, typebot: { - settings: deepParseVariable(typebot.variables)(typebot.settings), - theme: deepParseVariable(typebot.variables)(typebot.theme), + id: typebot.id, + settings: deepParseVariable(newInitialState.typebot.variables)( + typebot.settings + ), + theme: deepParseVariable(newInitialState.typebot.variables)( + typebot.theme + ), }, messages, input, logic, + dynamicTheme: parseDynamicThemeReply(newInitialState), } satisfies ChatReply } +const getTypebot = async ({ + typebot, + isPreview, +}: Pick): Promise => { + if (typeof typebot !== 'string') return typebot + const typebotQuery = isPreview + ? await prisma.typebot.findUnique({ + where: { id: typebot }, + select: { + id: true, + groups: true, + edges: true, + settings: true, + theme: true, + variables: true, + isArchived: true, + }, + }) + : await prisma.publicTypebot.findFirst({ + where: { typebot: { publicId: typebot } }, + select: { + groups: true, + edges: true, + settings: true, + theme: true, + variables: true, + typebotId: true, + typebot: { + select: { + isArchived: true, + isClosed: true, + workspace: { + select: { + id: true, + plan: true, + additionalChatsIndex: true, + chatsLimitFirstEmailSentAt: true, + chatsLimitSecondEmailSentAt: true, + customChatsLimit: true, + }, + }, + }, + }, + }, + }) + + const parsedTypebot = + typebotQuery && 'typebot' in typebotQuery + ? ({ + id: typebotQuery.typebotId, + ...omit(typebotQuery.typebot, 'workspace'), + ...omit(typebotQuery, 'typebot', 'typebotId'), + } as StartTypebot & Pick) + : (typebotQuery as StartTypebot & Pick) + + if ( + !parsedTypebot || + ('isArchived' in parsedTypebot && parsedTypebot.isArchived) + ) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Typebot not found', + }) + + if ('isClosed' in parsedTypebot && parsedTypebot.isClosed) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Typebot is closed', + }) + + const hasReachedLimit = + typebotQuery && 'typebot' in typebotQuery + ? await checkChatsUsage({ + typebotId: parsedTypebot.id, + workspace: typebotQuery.typebot.workspace, + }) + : false + + if (hasReachedLimit) + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'You have reached your chats limit', + }) + + return parsedTypebot +} + const getResult = async ({ - typebotId, + typebot, isPreview, resultId, startVariables, -}: Pick & { + isNewResultOnRefreshEnabled, +}: Pick & { startVariables: Variable[] + isNewResultOnRefreshEnabled: boolean }) => { - if (isPreview) return undefined + if (isPreview || typeof typebot !== 'string') return undefined const data = { isCompleted: false, - typebotId: typebotId, + typebotId: typebot, variables: { set: startVariables.filter((variable) => variable.value) }, } satisfies Prisma.ResultUncheckedCreateInput const select = { @@ -225,7 +282,7 @@ const getResult = async ({ hasStarted: true, } satisfies Prisma.ResultSelect return ( - resultId + resultId && !isNewResultOnRefreshEnabled ? await prisma.result.update({ where: { id: resultId }, data, @@ -237,3 +294,36 @@ const getResult = async ({ }) ) as Pick } + +const parseDynamicThemeInState = (theme: Theme) => { + const hostAvatarUrl = + theme.chat.hostAvatar?.isEnabled ?? true + ? theme.chat.hostAvatar?.url + : undefined + const guestAvatarUrl = + theme.chat.guestAvatar?.isEnabled ?? false + ? theme.chat.guestAvatar?.url + : undefined + if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{')) + return + return { + hostAvatarUrl: hostAvatarUrl?.startsWith('{{') ? hostAvatarUrl : undefined, + guestAvatarUrl: guestAvatarUrl?.startsWith('{{') + ? guestAvatarUrl + : undefined, + } +} + +const parseDynamicThemeReply = ( + state: SessionState | undefined +): ChatReply['dynamicTheme'] => { + if (!state?.dynamicTheme) return + return { + hostAvatarUrl: parseVariables(state?.typebot.variables)( + state.dynamicTheme.hostAvatarUrl + ), + guestAvatarUrl: parseVariables(state?.typebot.variables)( + state.dynamicTheme.guestAvatarUrl + ), + } +} diff --git a/apps/viewer/src/features/chat/api/utils/executeGroup.ts b/apps/viewer/src/features/chat/api/utils/executeGroup.ts index a61948716..40c6fb3b9 100644 --- a/apps/viewer/src/features/chat/api/utils/executeGroup.ts +++ b/apps/viewer/src/features/chat/api/utils/executeGroup.ts @@ -23,7 +23,7 @@ export const executeGroup = (state: SessionState, currentReply?: ChatReply) => async ( group: Group - ): Promise => { + ): Promise => { const messages: ChatReply['messages'] = currentReply?.messages ?? [] let logic: ChatReply['logic'] = currentReply?.logic let integrations: ChatReply['integrations'] = currentReply?.integrations @@ -72,8 +72,10 @@ export const executeGroup = integrations = executionResponse.integrations if (executionResponse.newSessionState) newSessionState = executionResponse.newSessionState - if (executionResponse.outgoingEdgeId) + if (executionResponse.outgoingEdgeId) { nextEdgeId = executionResponse.outgoingEdgeId + break + } } if (!nextEdgeId) return { messages, newSessionState, logic, integrations } diff --git a/apps/viewer/src/features/chat/api/utils/startBotFlow.ts b/apps/viewer/src/features/chat/api/utils/startBotFlow.ts index f6f1d4528..1065d182f 100644 --- a/apps/viewer/src/features/chat/api/utils/startBotFlow.ts +++ b/apps/viewer/src/features/chat/api/utils/startBotFlow.ts @@ -1,13 +1,26 @@ +import { TRPCError } from '@trpc/server' import { ChatReply, SessionState } from 'models' import { executeGroup } from './executeGroup' import { getNextGroup } from './getNextGroup' export const startBotFlow = async ( - state: SessionState -): Promise => { + state: SessionState, + startGroupId?: string +): Promise => { + if (startGroupId) { + const group = state.typebot.groups.find( + (group) => group.id === startGroupId + ) + if (!group) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "startGroupId doesn't exist", + }) + return executeGroup(state)(group) + } const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId - if (!firstEdgeId) return { messages: [] } + if (!firstEdgeId) return { messages: [], newSessionState: state } const nextGroup = getNextGroup(state)(firstEdgeId) - if (!nextGroup) return { messages: [] } + if (!nextGroup) return { messages: [], newSessionState: state } return executeGroup(state)(nextGroup.group) } diff --git a/apps/viewer/src/features/chat/chat.spec.ts b/apps/viewer/src/features/chat/chat.spec.ts index 30610a251..8c2461e7b 100644 --- a/apps/viewer/src/features/chat/chat.spec.ts +++ b/apps/viewer/src/features/chat/chat.spec.ts @@ -37,11 +37,10 @@ test('API chat execution should work on preview bot', async ({ request }) => { await request.post(`/api/v1/sendMessage`, { data: { startParams: { - typebotId, + typebot: typebotId, isPreview: true, }, - // TODO: replace with satisfies once compatible with playwright - } as SendMessageInput, + } satisfies SendMessageInput, }) ).json() expect(resultId).toBeUndefined() @@ -75,10 +74,9 @@ test('API chat execution should work on published bot', async ({ request }) => { await request.post(`/api/v1/sendMessage`, { data: { startParams: { - typebotId, + typebot: publicId, }, - // TODO: replace with satisfies once compatible with playwright - } as SendMessageInput, + } satisfies SendMessageInput, }) ).json() chatSessionId = sessionId diff --git a/apps/viewer/src/features/settings/settingsV2.spec.ts b/apps/viewer/src/features/settings/settingsV2.spec.ts index ebf6df209..299374502 100644 --- a/apps/viewer/src/features/settings/settingsV2.spec.ts +++ b/apps/viewer/src/features/settings/settingsV2.spec.ts @@ -27,8 +27,8 @@ test('Result should be overwritten on page refresh', async ({ page }) => { ]) const { resultId } = await response.json() expect(resultId).toBeDefined() - await expect(page.getByRole('textbox')).toBeVisible() + const [, secondResponse] = await Promise.all([ page.reload(), page.waitForResponse(/sendMessage/), diff --git a/apps/viewer/src/features/usage/utils/checkChatsUsage.ts b/apps/viewer/src/features/usage/utils/checkChatsUsage.ts index 38fd5415e..4d84e3e1c 100644 --- a/apps/viewer/src/features/usage/utils/checkChatsUsage.ts +++ b/apps/viewer/src/features/usage/utils/checkChatsUsage.ts @@ -4,30 +4,44 @@ import { sendAlmostReachedChatsLimitEmail, sendReachedChatsLimitEmail, } from 'emails' +import { Workspace } from 'models' import { env, getChatsLimit, isDefined } from 'utils' const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8 -export const checkChatsUsage = async (typebotId: string) => { - const typebot = await prisma.typebot.findUnique({ - where: { - id: typebotId, - }, - include: { - workspace: { - select: { - id: true, - plan: true, - additionalChatsIndex: true, - chatsLimitFirstEmailSentAt: true, - chatsLimitSecondEmailSentAt: true, - customChatsLimit: true, +export const checkChatsUsage = async (props: { + typebotId: string + workspace?: Pick< + Workspace, + | 'id' + | 'plan' + | 'additionalChatsIndex' + | 'chatsLimitFirstEmailSentAt' + | 'chatsLimitSecondEmailSentAt' + | 'customChatsLimit' + > +}) => { + const typebot = props.workspace + ? null + : await prisma.typebot.findUnique({ + where: { + id: props.typebotId, }, - }, - }, - }) + include: { + workspace: { + select: { + id: true, + plan: true, + additionalChatsIndex: true, + chatsLimitFirstEmailSentAt: true, + chatsLimitSecondEmailSentAt: true, + customChatsLimit: true, + }, + }, + }, + }) - const workspace = typebot?.workspace + const workspace = props.workspace || typebot?.workspace if (!workspace) return false diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts index 0dbebc4a8..7ceb517c6 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts @@ -26,7 +26,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === 'POST') { const typebotId = req.query.typebotId as string - const hasReachedLimit = await checkChatsUsage(typebotId) + const hasReachedLimit = await checkChatsUsage({ typebotId }) if (hasReachedLimit) return res.send({ result: null, hasReachedLimit }) const result = await prisma.result.create({ data: { diff --git a/apps/viewer/src/pages/next/[[...publicId]].tsx b/apps/viewer/src/pages/next/[[...publicId]].tsx index 0a0fd7b4c..8ea6cbd61 100644 --- a/apps/viewer/src/pages/next/[[...publicId]].tsx +++ b/apps/viewer/src/pages/next/[[...publicId]].tsx @@ -56,12 +56,12 @@ const getTypebotFromPublicId = async ( const typebot = (await prisma.typebot.findUnique({ where: { publicId }, select: { - id: true, theme: true, name: true, settings: true, isArchived: true, isClosed: true, + publicId: true, }, })) as TypebotPageV2Props['typebot'] | null if (isNotDefined(typebot)) return null diff --git a/apps/viewer/src/queries/getInitialChatReplyQuery.ts b/apps/viewer/src/queries/getInitialChatReplyQuery.ts deleted file mode 100644 index eac66e963..000000000 --- a/apps/viewer/src/queries/getInitialChatReplyQuery.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { InitialChatReply, SendMessageInput } from 'models' -import { sendRequest } from 'utils' - -type Props = { - typebotId: string - resultId?: string - prefilledVariables?: Record -} -export async function getInitialChatReplyQuery({ - typebotId, - resultId, - prefilledVariables, -}: Props) { - if (!typebotId) - throw new Error('Typebot ID is required to get initial messages') - - return sendRequest({ - method: 'POST', - url: `/api/v1/sendMessage`, - body: { - startParams: { - typebotId, - resultId, - prefilledVariables, - }, - } satisfies SendMessageInput, - }) -} diff --git a/packages/bot-engine/src/features/blocks/inputs/date/utils/parseReadableDate.ts b/packages/bot-engine/src/features/blocks/inputs/date/utils/parseReadableDate.ts index 694d4e7e4..fc04dd293 100644 --- a/packages/bot-engine/src/features/blocks/inputs/date/utils/parseReadableDate.ts +++ b/packages/bot-engine/src/features/blocks/inputs/date/utils/parseReadableDate.ts @@ -20,7 +20,6 @@ export const parseReadableDate = ({ const fromReadable = new Date( hasTime ? from : from.replace(/-/g, '/') ).toLocaleString(currentLocale, formatOptions) - console.log(to, to.replace(/-/g, '/')) const toReadable = new Date( hasTime ? to : to.replace(/-/g, '/') ).toLocaleString(currentLocale, formatOptions) diff --git a/packages/js/package.json b/packages/js/package.json index 218120283..b18f5e504 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "name": "@typebot.io/js", - "version": "0.0.0", - "description": "", + "version": "0.0.1", + "description": "Javascript library to display typebots on your website", "main": "dist/index.mjs", "types": "dist/index.d.ts", "scripts": { diff --git a/packages/js/src/assets/index.css b/packages/js/src/assets/index.css index 83e755e98..a849e710b 100644 --- a/packages/js/src/assets/index.css +++ b/packages/js/src/assets/index.css @@ -2,8 +2,7 @@ @tailwind components; @tailwind utilities; -:host, -:root { +:host { --typebot-container-bg-image: none; --typebot-container-bg-color: transparent; --typebot-container-font-family: 'Open Sans'; diff --git a/packages/js/src/components/Bot.tsx b/packages/js/src/components/Bot.tsx index a394f0ae2..2d1653e74 100644 --- a/packages/js/src/components/Bot.tsx +++ b/packages/js/src/components/Bot.tsx @@ -1,83 +1,128 @@ import { LiteBadge } from './LiteBadge' -import { createSignal, onCleanup, onMount, Show } from 'solid-js' -import { - getViewerUrl, - injectCustomHeadCode, - isDefined, - isNotEmpty, -} from 'utils' +import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' +import { getViewerUrl, injectCustomHeadCode, isEmpty, isNotEmpty } from 'utils' import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery' import { ConversationContainer } from './ConversationContainer' import css from '../assets/index.css' -import { InitialChatReply, StartParams } from 'models' +import { StartParams } from 'models' import { setIsMobile } from '@/utils/isMobileSignal' -import { BotContext } from '@/types' +import { BotContext, InitialChatReply } from '@/types' +import { ErrorMessage } from './ErrorMessage' +import { + getExistingResultIdFromSession, + setResultInSession, +} from '@/utils/sessionStorage' +import { setCssVariablesValue } from '@/utils/setCssVariablesValue' export type BotProps = StartParams & { - initialChatReply?: InitialChatReply apiHost?: string + onNewInputBlock?: (ids: { id: string; groupId: string }) => void + onAnswer?: (answer: { message: string; blockId: string }) => void + onInit?: () => void + onEnd?: () => void } export const Bot = (props: BotProps) => { const [initialChatReply, setInitialChatReply] = createSignal< InitialChatReply | undefined - >(props.initialChatReply) + >() + const [error, setError] = createSignal( + // eslint-disable-next-line solid/reactivity + isEmpty(isEmpty(props.apiHost) ? getViewerUrl() : props.apiHost) + ? new Error('process.env.NEXT_PUBLIC_VIEWER_URL is missing in env') + : undefined + ) + + const initializeBot = async () => { + const urlParams = new URLSearchParams(location.search) + props.onInit?.() + const prefilledVariables: { [key: string]: string } = {} + urlParams.forEach((value, key) => { + prefilledVariables[key] = value + }) + const { data, error } = await getInitialChatReplyQuery({ + typebot: props.typebot, + apiHost: props.apiHost, + isPreview: props.isPreview ?? false, + resultId: isNotEmpty(props.resultId) + ? props.resultId + : getExistingResultIdFromSession(), + startGroupId: props.startGroupId, + prefilledVariables: { + ...prefilledVariables, + ...props.prefilledVariables, + }, + }) + if (error && 'code' in error && typeof error.code === 'string') { + if (['BAD_REQUEST', 'FORBIDDEN'].includes(error.code)) + setError(new Error('This bot is now closed.')) + if (error.code === 'NOT_FOUND') setError(new Error('Typebot not found.')) + return + } + + if (!data) return setError(new Error("Couldn't initiate the chat")) + + if (data.resultId) setResultInSession(data.resultId) + setInitialChatReply(data) + + if (data.input?.id && props.onNewInputBlock) + props.onNewInputBlock({ + id: data.input.id, + groupId: data.input.groupId, + }) + const customHeadCode = data.typebot.settings.metadata.customHeadCode + if (customHeadCode) injectCustomHeadCode(customHeadCode) + } onMount(() => { - if (!props.typebotId) return - const initialChatReplyValue = initialChatReply() - if (isDefined(initialChatReplyValue)) { - const customHeadCode = - initialChatReplyValue.typebot.settings.metadata.customHeadCode - if (customHeadCode) injectCustomHeadCode(customHeadCode) - } else { - const urlParams = new URLSearchParams(location.search) - const prefilledVariables: { [key: string]: string } = {} - urlParams.forEach((value, key) => { - prefilledVariables[key] = value - }) - getInitialChatReplyQuery({ - typebotId: props.typebotId, - apiHost: props.apiHost, - isPreview: props.isPreview ?? false, - resultId: props.resultId, - prefilledVariables: { - ...prefilledVariables, - ...props.prefilledVariables, - }, - }).then((initialChatReply) => { - setInitialChatReply(initialChatReply) - }) - } + initializeBot().then() }) return ( - ( -

process.env.NEXT_PUBLIC_VIEWER_URL is missing in env

- )} - > + <> + + + {(error) => } + {(initialChatReply) => ( )} -
+ ) } type BotContentProps = { initialChatReply: InitialChatReply context: BotContext + onNewInputBlock?: (ids: { id: string; groupId: string }) => void + onAnswer?: (answer: { message: string; blockId: string }) => void + onEnd?: () => void } const BotContent = (props: BotContentProps) => { @@ -98,38 +143,39 @@ const BotContent = (props: BotContentProps) => { onMount(() => { injectCustomFont() - if (botContainer) { - resizeObserver.observe(botContainer) - } + if (!botContainer) return + resizeObserver.observe(botContainer) + }) + + createEffect(() => { + if (!botContainer) return + setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer) }) onCleanup(() => { - if (botContainer) { - resizeObserver.unobserve(botContainer) - } + if (!botContainer) return + resizeObserver.unobserve(botContainer) }) return ( - <> - -
-
- -
- - - +
+
+
- + + + +
) } diff --git a/packages/js/src/components/ConversationContainer/ChatChunk.tsx b/packages/js/src/components/ConversationContainer/ChatChunk.tsx index e80138b16..70796cb10 100644 --- a/packages/js/src/components/ConversationContainer/ChatChunk.tsx +++ b/packages/js/src/components/ConversationContainer/ChatChunk.tsx @@ -10,7 +10,9 @@ type Props = Pick & { settings: Settings inputIndex: number context: BotContext + onScrollToBottom: () => void onSubmit: (input: string) => void + onEnd?: () => void onSkip: () => void } @@ -23,6 +25,9 @@ export const ChatChunk = (props: Props) => { ? displayedMessageIndex() : displayedMessageIndex() + 1 ) + props.onScrollToBottom() + if (!props.input && displayedMessageIndex() === props.messages.length) + return props.onEnd?.() } return ( diff --git a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx index fb94e3a95..798f1e962 100644 --- a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -1,31 +1,73 @@ -import { ChatReply, InitialChatReply } from 'models' +import { ChatReply, Theme } from 'models' import { createSignal, For } from 'solid-js' import { sendMessageQuery } from '@/queries/sendMessageQuery' import { ChatChunk } from './ChatChunk' -import { BotContext } from '@/types' +import { BotContext, InitialChatReply } from '@/types' import { executeIntegrations } from '@/utils/executeIntegrations' import { executeLogic } from '@/utils/executeLogic' +const parseDynamicTheme = ( + theme: Theme, + dynamicTheme: ChatReply['dynamicTheme'] +): Theme => ({ + ...theme, + chat: { + ...theme.chat, + hostAvatar: theme.chat.hostAvatar + ? { + ...theme.chat.hostAvatar, + url: dynamicTheme?.hostAvatarUrl, + } + : undefined, + guestAvatar: theme.chat.guestAvatar + ? { + ...theme.chat.guestAvatar, + url: dynamicTheme?.guestAvatarUrl, + } + : undefined, + }, +}) + type Props = { initialChatReply: InitialChatReply context: BotContext + onNewInputBlock?: (ids: { id: string; groupId: string }) => void + onAnswer?: (answer: { message: string; blockId: string }) => void + onEnd?: () => void } export const ConversationContainer = (props: Props) => { + let bottomSpacer: HTMLDivElement | undefined const [chatChunks, setChatChunks] = createSignal([ { input: props.initialChatReply.input, messages: props.initialChatReply.messages, }, ]) + const [theme, setTheme] = createSignal( + parseDynamicTheme( + props.initialChatReply.typebot.theme, + props.initialChatReply.dynamicTheme + ) + ) const sendMessage = async (message: string) => { + const currentBlockId = chatChunks().at(-1)?.input?.id + if (currentBlockId && props.onAnswer) + props.onAnswer({ message, blockId: currentBlockId }) const data = await sendMessageQuery({ apiHost: props.context.apiHost, sessionId: props.initialChatReply.sessionId, message, }) if (!data) return + if (data.dynamicTheme) applyDynamicTheme(data.dynamicTheme) + if (data.input?.id && props.onNewInputBlock) { + props.onNewInputBlock({ + id: data.input.id, + groupId: data.input.groupId, + }) + } if (data.integrations) { executeIntegrations(data.integrations) } @@ -41,6 +83,17 @@ export const ConversationContainer = (props: Props) => { ]) } + const applyDynamicTheme = (dynamicTheme: ChatReply['dynamicTheme']) => { + setTheme((theme) => parseDynamicTheme(theme, dynamicTheme)) + } + + const autoScrollToBottom = () => { + if (!bottomSpacer) return + setTimeout(() => { + bottomSpacer?.scrollIntoView({ behavior: 'smooth' }) + }, 200) + } + return (
@@ -49,16 +102,26 @@ export const ConversationContainer = (props: Props) => { inputIndex={index()} messages={chatChunk.messages} input={chatChunk.input} - theme={props.initialChatReply.typebot.theme} + theme={theme()} settings={props.initialChatReply.typebot.settings} onSubmit={sendMessage} + onScrollToBottom={autoScrollToBottom} onSkip={() => { // TODO: implement skip }} + onEnd={props.onEnd} context={props.context} /> )} +
) } + +type BottomSpacerProps = { + ref: HTMLDivElement | undefined +} +const BottomSpacer = (props: BottomSpacerProps) => { + return
+} diff --git a/packages/js/src/components/ErrorMessage.tsx b/packages/js/src/components/ErrorMessage.tsx new file mode 100644 index 000000000..82d224702 --- /dev/null +++ b/packages/js/src/components/ErrorMessage.tsx @@ -0,0 +1,10 @@ +type Props = { + error: Error +} +export const ErrorMessage = (props: Props) => { + return ( +
+

{props.error.message}

+
+ ) +} diff --git a/packages/js/src/components/InputChatBlock.tsx b/packages/js/src/components/InputChatBlock.tsx index 2dcd06545..80d679648 100644 --- a/packages/js/src/components/InputChatBlock.tsx +++ b/packages/js/src/components/InputChatBlock.tsx @@ -44,7 +44,7 @@ export const InputChatBlock = (props: Props) => { const handleSubmit = async ({ label, value }: InputSubmitContent) => { setAnswer(label ?? value) - props.onSubmit(value) + props.onSubmit(value ?? label) } return ( @@ -96,37 +96,40 @@ const Input = (props: { @@ -146,7 +149,8 @@ const Input = (props: { diff --git a/packages/js/src/components/LiteBadge.tsx b/packages/js/src/components/LiteBadge.tsx index e7343e9cf..af153bb40 100644 --- a/packages/js/src/components/LiteBadge.tsx +++ b/packages/js/src/components/LiteBadge.tsx @@ -38,7 +38,7 @@ export const LiteBadge = (props: Props) => { href={'https://www.typebot.io/?utm_source=litebadge'} target="_blank" rel="noopener noreferrer" - class="fixed py-1 px-2 bg-white z-50 rounded shadow-md lite-badge" + class="absolute py-1 px-2 bg-white z-50 rounded shadow-md lite-badge text-gray-900" style={{ bottom: '20px' }} id="lite-badge" > diff --git a/packages/js/src/components/avatars/Avatar.tsx b/packages/js/src/components/avatars/Avatar.tsx index 7a1cdd3da..22746b048 100644 --- a/packages/js/src/components/avatars/Avatar.tsx +++ b/packages/js/src/components/avatars/Avatar.tsx @@ -1,24 +1,25 @@ import { isMobile } from '@/utils/isMobileSignal' import { Show } from 'solid-js' +import { isNotEmpty } from 'utils' import { DefaultAvatar } from './DefaultAvatar' export const Avatar = (props: { avatarSrc?: string }) => ( - - }> - {(currentAvatarSrc) => ( -
- Bot avatar -
- )} -
+ } + > +
+ Bot avatar +
) diff --git a/packages/js/src/components/inputs/ShortTextInput.tsx b/packages/js/src/components/inputs/ShortTextInput.tsx index e4948fccd..1cc05f10c 100644 --- a/packages/js/src/components/inputs/ShortTextInput.tsx +++ b/packages/js/src/components/inputs/ShortTextInput.tsx @@ -1,4 +1,3 @@ -import { isMobile } from '@/utils/isMobileSignal' import { splitProps } from 'solid-js' import { JSX } from 'solid-js/jsx-runtime' @@ -9,13 +8,13 @@ type ShortTextInputProps = { export const ShortTextInput = (props: ShortTextInputProps) => { const [local, others] = splitProps(props, ['ref', 'onInput']) + return ( local.onInput(e.currentTarget.value)} {...others} /> diff --git a/packages/js/src/demo/App.tsx b/packages/js/src/demo/App.tsx index 355848cae..14ee399c0 100644 --- a/packages/js/src/demo/App.tsx +++ b/packages/js/src/demo/App.tsx @@ -3,9 +3,6 @@ import type { Component } from 'solid-js' export const App: Component = () => { return ( - + ) } diff --git a/packages/js/src/features/blocks/inputs/date/components/DateForm.tsx b/packages/js/src/features/blocks/inputs/date/components/DateForm.tsx index 44779f782..0127a78c1 100644 --- a/packages/js/src/features/blocks/inputs/date/components/DateForm.tsx +++ b/packages/js/src/features/blocks/inputs/date/components/DateForm.tsx @@ -12,7 +12,7 @@ type Props = { export const DateForm = (props: Props) => { const [inputValues, setInputValues] = createSignal({ from: '', to: '' }) return ( -
+
void + block: EmailInputBlock + defaultValue?: string hasGuestAvatar: boolean + onSubmit: (value: InputSubmitContent) => void } export const EmailInput = (props: Props) => { - const [inputValue, setInputValue] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ?? '' - ) + const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') let inputRef: HTMLInputElement | undefined const handleInput = (inputValue: string) => setInputValue(inputValue) @@ -30,6 +29,10 @@ export const EmailInput = (props: Props) => { if (e.key === 'Enter') submit() } + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + return (
void hasGuestAvatar: boolean } export const NumberInput = (props: NumberInputProps) => { - const [inputValue, setInputValue] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ?? '' - ) + const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') let inputRef: HTMLInputElement | undefined const handleInput = (inputValue: string) => setInputValue(inputValue) @@ -30,6 +29,10 @@ export const NumberInput = (props: NumberInputProps) => { if (e.key === 'Enter') submit() } + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + return (
void hasGuestAvatar: boolean } export const PhoneInput = (props: PhoneInputProps) => { const [selectedCountryCode, setSelectedCountryCode] = createSignal('INT') - const [inputValue, setInputValue] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ?? '' - ) + const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') let inputRef: HTMLInputElement | undefined const handleInput = (inputValue: string | undefined) => { @@ -47,11 +45,13 @@ export const PhoneInput = (props: PhoneInputProps) => { setSelectedCountryCode(event.currentTarget.value) } + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + return (
void } export const RatingForm = (props: Props) => { const [rating, setRating] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ? Number(props.block.prefilledValue) : undefined + props.defaultValue ? Number(props.defaultValue) : undefined ) const handleSubmit = (e: SubmitEvent) => { e.preventDefault() - if (isNotDefined(rating)) return - props.onSubmit({ value: rating.toString() }) + const selectedRating = rating() + if (isNotDefined(selectedRating)) return + props.onSubmit({ value: selectedRating.toString() }) } const handleClick = (rating: number) => { diff --git a/packages/js/src/features/blocks/inputs/textInput/components/TextInput.tsx b/packages/js/src/features/blocks/inputs/textInput/components/TextInput.tsx index 099432e03..4d8c8a6d8 100644 --- a/packages/js/src/features/blocks/inputs/textInput/components/TextInput.tsx +++ b/packages/js/src/features/blocks/inputs/textInput/components/TextInput.tsx @@ -1,20 +1,19 @@ import { Textarea, ShortTextInput } from '@/components/inputs' import { SendButton } from '@/components/SendButton' import { InputSubmitContent } from '@/types' +import { isMobile } from '@/utils/isMobileSignal' import { TextInputBlock } from 'models' -import { createSignal } from 'solid-js' +import { createSignal, onMount } from 'solid-js' type Props = { - block: TextInputBlock & { prefilledValue?: string } - onSubmit: (value: InputSubmitContent) => void + block: TextInputBlock + defaultValue?: string hasGuestAvatar: boolean + onSubmit: (value: InputSubmitContent) => void } export const TextInput = (props: Props) => { - const [inputValue, setInputValue] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ?? '' - ) + const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined const handleInput = (inputValue: string) => setInputValue(inputValue) @@ -31,6 +30,10 @@ export const TextInput = (props: Props) => { if (e.key === 'Enter') submit() } + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + return (
void hasGuestAvatar: boolean } export const UrlInput = (props: Props) => { - const [inputValue, setInputValue] = createSignal( - // eslint-disable-next-line solid/reactivity - props.block.prefilledValue ?? '' - ) + const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined const handleInput = (inputValue: string) => { @@ -36,6 +35,10 @@ export const UrlInput = (props: Props) => { if (e.key === 'Enter') submit() } + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + return (
{ 'onClose', 'previewMessage', 'onPreviewMessageClick', - 'button', + 'theme', ]) const [prefilledVariables, setPrefilledVariables] = createSignal( // eslint-disable-next-line solid/reactivity @@ -106,13 +106,13 @@ export const Bubble = (props: BubbleProps) => { @@ -126,7 +126,7 @@ export const Bubble = (props: BubbleProps) => { 'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px', }} class={ - 'absolute bottom-20 sm:right-4 rounded-lg bg-white w-full sm:w-[400px] max-h-[704px] ' + + 'absolute bottom-20 sm:right-4 rounded-lg w-full sm:w-[400px] max-h-[704px] ' + (isBotOpened() ? 'opacity-1' : 'opacity-0 pointer-events-none') } > diff --git a/packages/js/src/features/bubble/components/BubbleButton.tsx b/packages/js/src/features/bubble/components/BubbleButton.tsx index a9eb916ba..e194b7dc8 100644 --- a/packages/js/src/features/bubble/components/BubbleButton.tsx +++ b/packages/js/src/features/bubble/components/BubbleButton.tsx @@ -1,7 +1,7 @@ import { Show } from 'solid-js' -import { ButtonParams } from '../types' +import { ButtonTheme } from '../types' -type Props = ButtonParams & { +type Props = ButtonTheme & { isBotOpened: boolean toggleBot: () => void } diff --git a/packages/js/src/features/bubble/components/PreviewMessage.tsx b/packages/js/src/features/bubble/components/PreviewMessage.tsx index 7a98053d9..95a45ea6b 100644 --- a/packages/js/src/features/bubble/components/PreviewMessage.tsx +++ b/packages/js/src/features/bubble/components/PreviewMessage.tsx @@ -1,14 +1,14 @@ import { createSignal } from 'solid-js' -import { BubbleParams, PreviewMessageParams } from '../types' +import { PreviewMessageParams, PreviewMessageTheme } from '../types' export type PreviewMessageProps = Pick< PreviewMessageParams, - 'avatarUrl' | 'message' | 'style' -> & - Pick & { - onClick: () => void - onCloseClick: () => void - } + 'avatarUrl' | 'message' +> & { + previewMessageTheme?: PreviewMessageTheme + onClick: () => void + onCloseClick: () => void +} const defaultFontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'" @@ -23,9 +23,11 @@ export const PreviewMessage = (props: PreviewMessageProps) => { onClick={props.onClick} class="absolute bottom-20 right-4 w-64 rounded-md duration-200 flex items-center gap-4 shadow-md animate-fade-in cursor-pointer hover:shadow-lg p-4" style={{ - 'font-family': props.style?.fontFamily ?? defaultFontFamily, - 'background-color': props.style?.backgroundColor ?? '#F7F8FF', - color: props.style?.color ?? '#303235', + 'font-family': + props.previewMessageTheme?.fontFamily ?? defaultFontFamily, + 'background-color': + props.previewMessageTheme?.backgroundColor ?? '#F7F8FF', + color: props.previewMessageTheme?.color ?? '#303235', }} onMouseEnter={() => setIsPreviewMessageHovered(true)} onMouseLeave={() => setIsPreviewMessageHovered(false)} @@ -40,8 +42,9 @@ export const PreviewMessage = (props: PreviewMessageProps) => { return props.onCloseClick() }} style={{ - 'background-color': props.style?.closeButtonBgColor ?? '#F7F8FF', - color: props.style?.closeButtonColor ?? '#303235', + 'background-color': + props.previewMessageTheme?.closeButtonBgColor ?? '#F7F8FF', + color: props.previewMessageTheme?.closeButtonColor ?? '#303235', }} > +export type PreviewMessageTheme = { + backgroundColor?: string + color?: string + fontFamily?: string + closeButtonBgColor?: string + closeButtonColor?: string +} diff --git a/packages/js/src/features/popup/components/Popup.tsx b/packages/js/src/features/popup/components/Popup.tsx index b75350454..6dce72659 100644 --- a/packages/js/src/features/popup/components/Popup.tsx +++ b/packages/js/src/features/popup/components/Popup.tsx @@ -1,5 +1,12 @@ import styles from '../../../assets/index.css' -import { createSignal, onMount, Show, splitProps, onCleanup } from 'solid-js' +import { + createSignal, + onMount, + Show, + splitProps, + onCleanup, + createEffect, +} from 'solid-js' import { Bot, BotProps } from '../../../components/Bot' import { CommandData } from '@/features/commands' import { isDefined } from 'utils' @@ -7,6 +14,8 @@ import { PopupParams } from '../types' export type PopupProps = BotProps & PopupParams & { + defaultOpen?: boolean + isOpen?: boolean onOpen?: () => void onClose?: () => void } @@ -18,7 +27,9 @@ export const Popup = (props: PopupProps) => { 'onOpen', 'onClose', 'autoShowDelay', - 'style', + 'theme', + 'isOpen', + 'defaultOpen', ]) const [prefilledVariables, setPrefilledVariables] = createSignal( @@ -26,10 +37,14 @@ export const Popup = (props: PopupProps) => { botProps.prefilledVariables ) - const [isBotOpened, setIsBotOpened] = createSignal(false) + const [isBotOpened, setIsBotOpened] = createSignal( + // eslint-disable-next-line solid/reactivity + popupProps.isOpen ?? popupProps.defaultOpen ?? false + ) onMount(() => { - window.addEventListener('click', processWindowClick) + document.addEventListener('pointerdown', processWindowClick) + botContainer?.addEventListener('pointerdown', stopPropagation) window.addEventListener('message', processIncomingEvent) const autoShowDelay = popupProps.autoShowDelay if (isDefined(autoShowDelay)) { @@ -39,16 +54,25 @@ export const Popup = (props: PopupProps) => { } }) - onCleanup(() => { - window.removeEventListener('message', processIncomingEvent) - window.removeEventListener('click', processWindowClick) + createEffect(() => { + const isOpen = popupProps.isOpen + if (isDefined(isOpen)) setIsBotOpened(isOpen) }) - const processWindowClick = (event: MouseEvent) => { - if (!botContainer || botContainer.contains(event.target as Node)) return + onCleanup(() => { + document.removeEventListener('pointerdown', processWindowClick) + botContainer?.removeEventListener('pointerdown', stopPropagation) + window.removeEventListener('message', processIncomingEvent) + }) + + const processWindowClick = () => { setIsBotOpened(false) } + const stopPropagation = (event: MouseEvent) => { + event.stopPropagation() + } + const processIncomingEvent = (event: MessageEvent) => { const { data } = event if (!data.isFromTypebot) return @@ -63,16 +87,19 @@ export const Popup = (props: PopupProps) => { } const openBot = () => { - setIsBotOpened(true) if (isBotOpened()) popupProps.onOpen?.() + if (isDefined(props.isOpen)) return + setIsBotOpened(true) } const closeBot = () => { - setIsBotOpened(false) if (isBotOpened()) popupProps.onClose?.() + if (isDefined(props.isOpen)) return + setIsBotOpened(false) } const toggleBot = () => { + if (isDefined(props.isOpen)) return isBotOpened() ? closeBot() : openBot() } @@ -85,15 +112,11 @@ export const Popup = (props: PopupProps) => { aria-modal="true" > -
+
diff --git a/packages/js/src/features/popup/types.ts b/packages/js/src/features/popup/types.ts index d73c74bbe..17971956d 100644 --- a/packages/js/src/features/popup/types.ts +++ b/packages/js/src/features/popup/types.ts @@ -1,6 +1,6 @@ export type PopupParams = { autoShowDelay?: number - style?: { + theme?: { width?: string backgroundColor?: string } diff --git a/packages/js/src/queries/getInitialChatReplyQuery.ts b/packages/js/src/queries/getInitialChatReplyQuery.ts index 6f5d79ea9..b624fcb09 100644 --- a/packages/js/src/queries/getInitialChatReplyQuery.ts +++ b/packages/js/src/queries/getInitialChatReplyQuery.ts @@ -1,28 +1,31 @@ -import { InitialChatReply, SendMessageInput, StartParams } from 'models' -import { getViewerUrl, sendRequest } from 'utils' +import { InitialChatReply } from '@/types' +import { SendMessageInput, StartParams } from 'models' +import { getViewerUrl, isEmpty, sendRequest } from 'utils' export async function getInitialChatReplyQuery({ - typebotId, + typebot, isPreview, apiHost, prefilledVariables, + startGroupId, + resultId, }: StartParams & { apiHost?: string }) { - if (!typebotId) + if (!typebot) throw new Error('Typebot ID is required to get initial messages') - const response = await sendRequest({ + return sendRequest({ method: 'POST', - url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`, + url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`, body: { startParams: { isPreview, - typebotId, + typebot, prefilledVariables, + startGroupId, + resultId, }, } satisfies SendMessageInput, }) - - return response.data } diff --git a/packages/js/src/queries/sendMessageQuery.ts b/packages/js/src/queries/sendMessageQuery.ts index 1061c93be..98dc0e098 100644 --- a/packages/js/src/queries/sendMessageQuery.ts +++ b/packages/js/src/queries/sendMessageQuery.ts @@ -1,5 +1,5 @@ import { ChatReply, SendMessageInput } from 'models' -import { getViewerUrl, sendRequest } from 'utils' +import { getViewerUrl, isEmpty, sendRequest } from 'utils' export async function sendMessageQuery({ apiHost, @@ -7,7 +7,7 @@ export async function sendMessageQuery({ }: SendMessageInput & { apiHost?: string }) { const response = await sendRequest({ method: 'POST', - url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`, + url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`, body, }) diff --git a/packages/js/src/types.ts b/packages/js/src/types.ts index 516ff2f1e..a5ee3f815 100644 --- a/packages/js/src/types.ts +++ b/packages/js/src/types.ts @@ -1,3 +1,5 @@ +import { ChatReply } from 'models' + export type InputSubmitContent = { label?: string value: string @@ -5,7 +7,12 @@ export type InputSubmitContent = { export type BotContext = { typebotId: string - resultId: string + resultId?: string isPreview: boolean apiHost?: string } + +export type InitialChatReply = ChatReply & { + typebot: NonNullable + sessionId: NonNullable +} diff --git a/packages/js/src/utils/sessionStorage.ts b/packages/js/src/utils/sessionStorage.ts new file mode 100644 index 000000000..063e8c021 --- /dev/null +++ b/packages/js/src/utils/sessionStorage.ts @@ -0,0 +1,17 @@ +const sessionStorageKey = 'resultId' + +export const getExistingResultIdFromSession = () => { + try { + return sessionStorage.getItem(sessionStorageKey) ?? undefined + } catch { + /* empty */ + } +} + +export const setResultInSession = (resultId: string) => { + try { + return sessionStorage.setItem(sessionStorageKey, resultId) + } catch { + /* empty */ + } +} diff --git a/packages/js/src/utils/setCssVariablesValue.ts b/packages/js/src/utils/setCssVariablesValue.ts new file mode 100644 index 000000000..24c119898 --- /dev/null +++ b/packages/js/src/utils/setCssVariablesValue.ts @@ -0,0 +1,144 @@ +import { + Background, + BackgroundType, + ChatTheme, + ContainerColors, + GeneralTheme, + InputColors, + Theme, +} from 'models' + +const cssVariableNames = { + general: { + bgImage: '--typebot-container-bg-image', + bgColor: '--typebot-container-bg-color', + fontFamily: '--typebot-container-font-family', + }, + chat: { + hostBubbles: { + bgColor: '--typebot-host-bubble-bg-color', + color: '--typebot-host-bubble-color', + }, + guestBubbles: { + bgColor: '--typebot-guest-bubble-bg-color', + color: '--typebot-guest-bubble-color', + }, + inputs: { + bgColor: '--typebot-input-bg-color', + color: '--typebot-input-color', + placeholderColor: '--typebot-input-placeholder-color', + }, + buttons: { + bgColor: '--typebot-button-bg-color', + color: '--typebot-button-color', + }, + }, +} + +export const setCssVariablesValue = ( + theme: Theme | undefined, + container: HTMLDivElement +) => { + if (!theme) return + const documentStyle = container?.style + if (!documentStyle) return + if (theme.general) setGeneralTheme(theme.general, documentStyle) + if (theme.chat) setChatTheme(theme.chat, documentStyle) +} + +const setGeneralTheme = ( + generalTheme: GeneralTheme, + documentStyle: CSSStyleDeclaration +) => { + const { background, font } = generalTheme + if (background) setTypebotBackground(background, documentStyle) + if (font) documentStyle.setProperty(cssVariableNames.general.fontFamily, font) +} + +const setChatTheme = ( + chatTheme: ChatTheme, + documentStyle: CSSStyleDeclaration +) => { + const { hostBubbles, guestBubbles, buttons, inputs } = chatTheme + if (hostBubbles) setHostBubbles(hostBubbles, documentStyle) + if (guestBubbles) setGuestBubbles(guestBubbles, documentStyle) + if (buttons) setButtons(buttons, documentStyle) + if (inputs) setInputs(inputs, documentStyle) +} + +const setHostBubbles = ( + hostBubbles: ContainerColors, + documentStyle: CSSStyleDeclaration +) => { + if (hostBubbles.backgroundColor) + documentStyle.setProperty( + cssVariableNames.chat.hostBubbles.bgColor, + hostBubbles.backgroundColor + ) + if (hostBubbles.color) + documentStyle.setProperty( + cssVariableNames.chat.hostBubbles.color, + hostBubbles.color + ) +} + +const setGuestBubbles = ( + guestBubbles: ContainerColors, + documentStyle: CSSStyleDeclaration +) => { + if (guestBubbles.backgroundColor) + documentStyle.setProperty( + cssVariableNames.chat.guestBubbles.bgColor, + guestBubbles.backgroundColor + ) + if (guestBubbles.color) + documentStyle.setProperty( + cssVariableNames.chat.guestBubbles.color, + guestBubbles.color + ) +} + +const setButtons = ( + buttons: ContainerColors, + documentStyle: CSSStyleDeclaration +) => { + if (buttons.backgroundColor) + documentStyle.setProperty( + cssVariableNames.chat.buttons.bgColor, + buttons.backgroundColor + ) + if (buttons.color) + documentStyle.setProperty( + cssVariableNames.chat.buttons.color, + buttons.color + ) +} + +const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => { + if (inputs.backgroundColor) + documentStyle.setProperty( + cssVariableNames.chat.inputs.bgColor, + inputs.backgroundColor + ) + if (inputs.color) + documentStyle.setProperty(cssVariableNames.chat.inputs.color, inputs.color) + if (inputs.placeholderColor) + documentStyle.setProperty( + cssVariableNames.chat.inputs.placeholderColor, + inputs.placeholderColor + ) +} + +const setTypebotBackground = ( + background: Background, + documentStyle: CSSStyleDeclaration +) => { + documentStyle.setProperty( + background?.type === BackgroundType.IMAGE + ? cssVariableNames.general.bgImage + : cssVariableNames.general.bgColor, + background.type === BackgroundType.NONE + ? 'transparent' + : background.content ?? '#ffffff' + ) +} diff --git a/packages/models/src/features/chat.ts b/packages/models/src/features/chat.ts index f43282a0a..bb8a63648 100644 --- a/packages/models/src/features/chat.ts +++ b/packages/models/src/features/chat.ts @@ -24,8 +24,14 @@ const typebotInSessionStateSchema = publicTypebotSchema.pick({ variables: true, }) +const dynamicThemeSchema = z.object({ + hostAvatarUrl: z.string().optional(), + guestAvatarUrl: z.string().optional(), +}) + export const sessionStateSchema = z.object({ typebot: typebotInSessionStateSchema, + dynamicTheme: dynamicThemeSchema.optional(), linkedTypebots: z.object({ typebots: z.array(typebotInSessionStateSchema), queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })), @@ -95,11 +101,21 @@ const codeToExecuteSchema = z.object({ ), }) +const startTypebotSchema = typebotSchema.pick({ + id: true, + groups: true, + edges: true, + variables: true, + settings: true, + theme: true, +}) + const startParamsSchema = z.object({ - typebotId: z.string({ - description: - '[How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)', - }), + typebot: startTypebotSchema + .or(z.string()) + .describe( + 'Either a Typebot ID or a Typebot object. If you provide a Typebot object, it will be executed in preview mode. ([How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)).' + ), isPreview: z .boolean() .optional() @@ -110,7 +126,16 @@ const startParamsSchema = z.object({ .string() .optional() .describe("Provide it if you'd like to overwrite an existing result."), - prefilledVariables: z.record(z.unknown()).optional(), + startGroupId: z + .string() + .optional() + .describe('Start chat from a specific group.'), + prefilledVariables: z + .record(z.unknown()) + .optional() + .describe( + '[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)' + ), }) export const sendMessageInputSchema = z.object({ @@ -158,25 +183,20 @@ export const chatReplySchema = z.object({ }) .optional(), sessionId: z.string().optional(), - typebot: typebotSchema.pick({ theme: true, settings: true }).optional(), + typebot: typebotSchema + .pick({ id: true, theme: true, settings: true }) + .optional(), resultId: z.string().optional(), + dynamicTheme: dynamicThemeSchema.optional(), }) -export const initialChatReplySchema = z - .object({ - sessionId: z.string(), - resultId: z.string(), - typebot: typebotSchema.pick({ theme: true, settings: true }), - }) - .and(chatReplySchema) - export type ChatSession = z.infer export type SessionState = z.infer export type TypebotInSession = z.infer export type ChatReply = z.infer -export type InitialChatReply = z.infer export type ChatMessage = z.infer export type SendMessageInput = z.infer export type CodeToExecute = z.infer export type StartParams = z.infer export type RuntimeOptions = z.infer +export type StartTypebot = z.infer diff --git a/packages/react/package.json b/packages/react/package.json index 74305c252..32b8b8d5e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@typebot.io/react", - "version": "1.0.0", - "description": "", + "version": "0.0.1", + "description": "React library to display typebots on your website", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/packages/react/src/Bubble.tsx b/packages/react/src/Bubble.tsx index 104781f71..27c956d48 100644 --- a/packages/react/src/Bubble.tsx +++ b/packages/react/src/Bubble.tsx @@ -1,5 +1,8 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import type { BubbleProps } from '@typebot.io/js' +import { defaultBubbleProps } from './constants' + +type Props = BubbleProps & { style?: React.CSSProperties; className?: string } declare global { namespace JSX { @@ -7,18 +10,54 @@ declare global { 'typebot-bubble': React.DetailedHTMLProps< React.HTMLAttributes, HTMLElement - > + > & { class?: string } } } } -export const Bubble = (props: BubbleProps) => { +export const Bubble = ({ style, className, ...props }: Props) => { + const ref = useRef<(HTMLDivElement & Props) | null>(null) + useEffect(() => { ;(async () => { const { registerBubbleComponent } = await import('@typebot.io/js') - registerBubbleComponent(props) + registerBubbleComponent(defaultBubbleProps) })() - }, [props]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return + useEffect(() => { + if (!ref.current) return + ref.current.typebot = props.typebot + ref.current.prefilledVariables = props.prefilledVariables + ref.current.onClose = props.onClose + ref.current.onOpen = props.onOpen + ref.current.onNewInputBlock = props.onNewInputBlock + ref.current.onAnswer = props.onAnswer + ref.current.onPreviewMessageClick = props.onPreviewMessageClick + ref.current.onEnd = props.onEnd + ref.current.onInit = props.onInit + }, [ + props.onAnswer, + props.onClose, + props.onNewInputBlock, + props.onOpen, + props.onPreviewMessageClick, + props.prefilledVariables, + props.typebot, + props.onEnd, + props.onInit, + ]) + + return ( + + ) } diff --git a/packages/react/src/Popup.tsx b/packages/react/src/Popup.tsx index 94bc8770e..da10aed2e 100644 --- a/packages/react/src/Popup.tsx +++ b/packages/react/src/Popup.tsx @@ -1,5 +1,8 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import type { PopupProps } from '@typebot.io/js' +import { defaultPopupProps } from './constants' + +type Props = PopupProps & { style?: React.CSSProperties; className?: string } declare global { namespace JSX { @@ -7,18 +10,54 @@ declare global { 'typebot-popup': React.DetailedHTMLProps< React.HTMLAttributes, HTMLElement - > + > & { class?: string } } } } -export const Popup = (props: PopupProps) => { +export const Popup = ({ style, className, ...props }: Props) => { + const ref = useRef<(HTMLDivElement & Props) | null>(null) + useEffect(() => { ;(async () => { const { registerPopupComponent } = await import('@typebot.io/js') - registerPopupComponent(props) + registerPopupComponent(defaultPopupProps) })() - }, [props]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return + useEffect(() => { + if (!ref.current) return + ref.current.typebot = props.typebot + ref.current.prefilledVariables = props.prefilledVariables + ref.current.onClose = props.onClose + ref.current.onOpen = props.onOpen + ref.current.onNewInputBlock = props.onNewInputBlock + ref.current.onAnswer = props.onAnswer + ref.current.onEnd = props.onEnd + ref.current.onInit = props.onInit + }, [ + props.onAnswer, + props.onClose, + props.onEnd, + props.onNewInputBlock, + props.onOpen, + props.onInit, + props.prefilledVariables, + props.typebot, + ]) + + return ( + + ) } diff --git a/packages/react/src/Standard.tsx b/packages/react/src/Standard.tsx index 6a67236b6..4f09e7000 100644 --- a/packages/react/src/Standard.tsx +++ b/packages/react/src/Standard.tsx @@ -1,7 +1,8 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import type { BotProps } from '@typebot.io/js' +import { defaultBotProps } from './constants' -type Props = BotProps +type Props = BotProps & { style?: React.CSSProperties; className?: string } declare global { namespace JSX { @@ -9,19 +10,48 @@ declare global { 'typebot-standard': React.DetailedHTMLProps< React.HTMLAttributes, HTMLElement - > + > & { class?: string } } } } -export const Standard = (props: Props) => { +export const Standard = ({ style, className, ...props }: Props) => { + const ref = useRef<(HTMLDivElement & Props) | null>(null) + useEffect(() => { ;(async () => { const { registerStandardComponent } = await import('@typebot.io/js') - registerStandardComponent(props) + registerStandardComponent(defaultBotProps) })() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - return + useEffect(() => { + if (!ref.current) return + ref.current.typebot = props.typebot + ref.current.prefilledVariables = props.prefilledVariables + ref.current.onNewInputBlock = props.onNewInputBlock + ref.current.onAnswer = props.onAnswer + ref.current.onEnd = props.onEnd + ref.current.onInit = props.onInit + }, [ + props.onAnswer, + props.onNewInputBlock, + props.prefilledVariables, + props.typebot, + props.onEnd, + props.onInit, + ]) + + return ( + + ) } diff --git a/packages/react/src/constants.ts b/packages/react/src/constants.ts new file mode 100644 index 000000000..92715a4dd --- /dev/null +++ b/packages/react/src/constants.ts @@ -0,0 +1,32 @@ +import type { BotProps, PopupProps, BubbleProps } from '@typebot.io/js' + +export const defaultBotProps: BotProps = { + typebot: '', + onNewInputBlock: undefined, + onAnswer: undefined, + onEnd: undefined, + onInit: undefined, + isPreview: undefined, + startGroupId: undefined, + prefilledVariables: undefined, + apiHost: undefined, + resultId: undefined, +} + +export const defaultPopupProps: PopupProps = { + ...defaultBotProps, + onClose: undefined, + onOpen: undefined, + theme: undefined, + autoShowDelay: undefined, + isOpen: undefined, + defaultOpen: undefined, +} + +export const defaultBubbleProps: BubbleProps = { + ...defaultBotProps, + onClose: undefined, + onOpen: undefined, + theme: undefined, + previewMessage: undefined, +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 5a2f06850..4dede090d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -5,6 +5,6 @@ import { Popup } from './Popup' export { Standard, Bubble, Popup } -export default { Standard, Bubble, Popup } +// export default { Standard, Bubble, Popup } export * from '@typebot.io/js/src/features/commands' diff --git a/packages/react/src/stories/bubble.stories.tsx b/packages/react/src/stories/bubble.stories.tsx index 1dd790b61..8217240b4 100644 --- a/packages/react/src/stories/bubble.stories.tsx +++ b/packages/react/src/stories/bubble.stories.tsx @@ -32,7 +32,7 @@ export const Default = () => {
{ message: 'Hello, I am a preview message', autoShowDelay: 3000, }} - button={{ - backgroundColor: '#FF7537', - icon: { - color: 'white', + theme={{ + button: { + backgroundColor: '#FF7537', + icon: { + color: 'white', + }, }, }} + isPreview />
) diff --git a/packages/react/src/stories/popup.stories.tsx b/packages/react/src/stories/popup.stories.tsx index 91d58e6c1..35f21c329 100644 --- a/packages/react/src/stories/popup.stories.tsx +++ b/packages/react/src/stories/popup.stories.tsx @@ -7,9 +7,10 @@ export const Default = () => { ) diff --git a/packages/react/src/stories/standard.stories.tsx b/packages/react/src/stories/standard.stories.tsx index 2b9671409..52aaf8d20 100644 --- a/packages/react/src/stories/standard.stories.tsx +++ b/packages/react/src/stories/standard.stories.tsx @@ -3,7 +3,11 @@ import { Standard } from '..' export const Default = () => { return (
- +
) }