diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsBlockSettings.tsx b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsBlockSettings.tsx index 129bf17b01..2a2db9e3b1 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsBlockSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsBlockSettings.tsx @@ -12,13 +12,15 @@ type Props = { } export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => { - const handleIsMultipleChange = (isMultipleChoice: boolean) => + const updateIsMultiple = (isMultipleChoice: boolean) => options && onOptionsChange({ ...options, isMultipleChoice }) - const handleButtonLabelChange = (buttonLabel: string) => + const updateIsSearchable = (isSearchable: boolean) => + options && onOptionsChange({ ...options, isSearchable }) + const updateButtonLabel = (buttonLabel: string) => options && onOptionsChange({ ...options, buttonLabel }) - const handleVariableChange = (variable?: Variable) => + const updateSaveVariable = (variable?: Variable) => options && onOptionsChange({ ...options, variableId: variable?.id }) - const handleDynamicVariableChange = (variable?: Variable) => + const updateDynamicDataVariable = (variable?: Variable) => options && onOptionsChange({ ...options, dynamicVariableId: variable?.id }) return ( @@ -26,18 +28,23 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => { + {options?.isMultipleChoice && ( )} - Dynamic items from variable:{' '} + Dynamic data:{' '} If defined, buttons will be dynamically displayed based on what the variable contains. @@ -45,7 +52,7 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => { @@ -54,7 +61,7 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => { diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index bf90b8ba23..47ac3d3ba8 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -295,6 +295,54 @@ "data" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string", + "enum": [ + "Workspace limit reached" + ] + }, + "data": { + "type": "object", + "properties": { + "chatsLimit": { + "type": "number" + }, + "storageLimit": { + "type": "number" + }, + "totalChatsUsed": { + "type": "number" + }, + "totalStorageUsed": { + "type": "number" + } + }, + "required": [ + "chatsLimit", + "storageLimit", + "totalChatsUsed", + "totalStorageUsed" + ], + "additionalProperties": false + } + }, + "required": [ + "userId", + "workspaceId", + "name", + "data" + ], + "additionalProperties": false } ] } @@ -798,6 +846,9 @@ }, "dynamicVariableId": { "type": "string" + }, + "isSearchable": { + "type": "boolean" } }, "required": [ @@ -1743,6 +1794,121 @@ "options" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "AB test" + ] + }, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "number", + "enum": [ + 2 + ] + }, + "path": { + "type": "string", + "enum": [ + "a" + ] + } + }, + "required": [ + "id", + "blockId", + "type", + "path" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "number", + "enum": [ + 2 + ] + }, + "path": { + "type": "string", + "enum": [ + "b" + ] + } + }, + "required": [ + "id", + "blockId", + "type", + "path" + ], + "additionalProperties": false + } + ] + }, + "options": { + "type": "object", + "properties": { + "aPercent": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "required": [ + "aPercent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "groupId", + "type", + "items", + "options" + ], + "additionalProperties": false } ] }, @@ -3075,6 +3241,9 @@ "customSeatsLimit": { "type": "number", "nullable": true + }, + "isQuarantined": { + "type": "boolean" } }, "required": [ @@ -3093,7 +3262,8 @@ "storageLimitSecondEmailSentAt", "customChatsLimit", "customStorageLimit", - "customSeatsLimit" + "customSeatsLimit", + "isQuarantined" ], "additionalProperties": false } @@ -3216,6 +3386,9 @@ "customSeatsLimit": { "type": "number", "nullable": true + }, + "isQuarantined": { + "type": "boolean" } }, "required": [ @@ -3234,7 +3407,8 @@ "storageLimitSecondEmailSentAt", "customChatsLimit", "customStorageLimit", - "customSeatsLimit" + "customSeatsLimit", + "isQuarantined" ], "additionalProperties": false } @@ -3374,6 +3548,9 @@ "customSeatsLimit": { "type": "number", "nullable": true + }, + "isQuarantined": { + "type": "boolean" } }, "required": [ @@ -3392,7 +3569,8 @@ "storageLimitSecondEmailSentAt", "customChatsLimit", "customStorageLimit", - "customSeatsLimit" + "customSeatsLimit", + "isQuarantined" ], "additionalProperties": false } @@ -4642,6 +4820,9 @@ "customSeatsLimit": { "type": "number", "nullable": true + }, + "isQuarantined": { + "type": "boolean" } }, "required": [ @@ -4660,7 +4841,8 @@ "storageLimitSecondEmailSentAt", "customChatsLimit", "customStorageLimit", - "customSeatsLimit" + "customSeatsLimit", + "isQuarantined" ], "additionalProperties": false } diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index da0aa9f527..211abae420 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -459,6 +459,9 @@ }, "dynamicVariableId": { "type": "string" + }, + "isSearchable": { + "type": "boolean" } }, "required": [ @@ -1404,6 +1407,121 @@ "options" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "AB test" + ] + }, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "number", + "enum": [ + 2 + ] + }, + "path": { + "type": "string", + "enum": [ + "a" + ] + } + }, + "required": [ + "id", + "blockId", + "type", + "path" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "number", + "enum": [ + 2 + ] + }, + "path": { + "type": "string", + "enum": [ + "b" + ] + } + }, + "required": [ + "id", + "blockId", + "type", + "path" + ], + "additionalProperties": false + } + ] + }, + "options": { + "type": "object", + "properties": { + "aPercent": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "required": [ + "aPercent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "groupId", + "type", + "items", + "options" + ], + "additionalProperties": false } ] }, @@ -3182,6 +3300,9 @@ }, "dynamicVariableId": { "type": "string" + }, + "isSearchable": { + "type": "boolean" } }, "required": [ diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 58e3de35b9..4aaf9647d4 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.0.40", + "version": "0.0.41", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/assets/index.css b/packages/embeds/js/src/assets/index.css index ac4ca45a1f..9355d173ec 100644 --- a/packages/embeds/js/src/assets/index.css +++ b/packages/embeds/js/src/assets/index.css @@ -286,3 +286,14 @@ textarea { .typebot-popup-blocked-toast { border-radius: var(--typebot-border-radius); } + +/* Hide scrollbar for Chrome, Safari and Opera */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} diff --git a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx index 78270b1227..ca982179ba 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx @@ -47,38 +47,42 @@ export const ChatChunk = (props: Props) => { return (
-
- 0 - } - > - - -
- - {(message) => ( - - )} - + 0}> +
+ 0 + } + > + + + +
+ + {(message) => ( + + )} + +
-
+ {props.input && displayedMessageIndex() === props.messages.length && ( @@ -159,30 +161,49 @@ const Input = (props: { onSubmit={onSubmit} /> - - - - - + + {(block) => ( + + + + + + + + + + + + + + + + + + + + + + + )} ) } + +const isButtonsBlock = ( + block: ChatReply['input'] +): ChoiceInputBlock | undefined => + block?.type === InputBlockType.CHOICE ? block : undefined diff --git a/packages/embeds/js/src/components/icons/CloseIcon.tsx b/packages/embeds/js/src/components/icons/CloseIcon.tsx new file mode 100644 index 0000000000..93827df09f --- /dev/null +++ b/packages/embeds/js/src/components/icons/CloseIcon.tsx @@ -0,0 +1,17 @@ +import { JSX } from 'solid-js/jsx-runtime' + +export const CloseIcon = (props: JSX.SvgSVGAttributes) => ( + + + + +) diff --git a/packages/embeds/js/src/components/inputs/SearchInput.tsx b/packages/embeds/js/src/components/inputs/SearchInput.tsx new file mode 100644 index 0000000000..55457b46a8 --- /dev/null +++ b/packages/embeds/js/src/components/inputs/SearchInput.tsx @@ -0,0 +1,43 @@ +import { Show, createSignal, splitProps } from 'solid-js' +import { JSX } from 'solid-js/jsx-runtime' +import { CloseIcon } from '../icons/CloseIcon' + +type Props = { + ref: HTMLInputElement | undefined + onInput: (value: string) => void + onClear: () => void +} & Omit, 'onInput'> + +export const SearchInput = (props: Props) => { + const [value, setValue] = createSignal('') + const [local, others] = splitProps(props, ['onInput', 'ref']) + + const changeValue = (value: string) => { + setValue(value) + local.onInput(value) + } + + const clearValue = () => { + setValue('') + props.onClear() + } + + return ( +
+ changeValue(e.currentTarget.value)} + {...others} + /> + 0}> + + +
+ ) +} diff --git a/packages/embeds/js/src/features/blocks/inputs/buttons/components/MultipleChoicesForm.tsx b/packages/embeds/js/src/features/blocks/inputs/buttons/components/MultipleChoicesForm.tsx index 95f162f180..351bbe86b0 100644 --- a/packages/embeds/js/src/features/blocks/inputs/buttons/components/MultipleChoicesForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/buttons/components/MultipleChoicesForm.tsx @@ -72,13 +72,11 @@ export const MultipleChoicesForm = (props: Props) => { )}
-
- {selectedIndices().length > 0 && ( - - {props.options?.buttonLabel ?? 'Send'} - - )} -
+ {selectedIndices().length > 0 && ( + + {props.options?.buttonLabel ?? 'Send'} + + )} ) } diff --git a/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableButtons.tsx b/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableButtons.tsx new file mode 100644 index 0000000000..6067a3100a --- /dev/null +++ b/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableButtons.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/components/Button' +import { SearchInput } from '@/components/inputs/SearchInput' +import { InputSubmitContent } from '@/types' +import { isMobile } from '@/utils/isMobileSignal' +import type { ChoiceInputBlock } from '@typebot.io/schemas' +import { For, createSignal, onMount } from 'solid-js' + +type Props = { + inputIndex: number + defaultItems: ChoiceInputBlock['items'] + onSubmit: (value: InputSubmitContent) => void +} + +export const SearchableButtons = (props: Props) => { + let inputRef: HTMLInputElement | undefined + const [filteredItems, setFilteredItems] = createSignal(props.defaultItems) + + onMount(() => { + if (!isMobile() && inputRef) inputRef.focus() + }) + + // eslint-disable-next-line solid/reactivity + const handleClick = (itemIndex: number) => () => + props.onSubmit({ value: filteredItems()[itemIndex].content ?? '' }) + + const filterItems = (inputValue: string) => { + setFilteredItems( + props.defaultItems.filter((item) => + item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase()) + ) + ) + } + + return ( +
+
+ setFilteredItems(props.defaultItems)} + /> +
+ +
+ + {(item, index) => ( + + + {props.inputIndex === 0 && props.defaultItems.length === 1 && ( + + + + + )} + + )} + +
+
+ ) +} diff --git a/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm.tsx b/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm.tsx new file mode 100644 index 0000000000..bc362aa861 --- /dev/null +++ b/packages/embeds/js/src/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm.tsx @@ -0,0 +1,131 @@ +import { SendButton } from '@/components/SendButton' +import { InputSubmitContent } from '@/types' +import { isMobile } from '@/utils/isMobileSignal' +import type { ChoiceInputBlock } from '@typebot.io/schemas' +import { createSignal, For } from 'solid-js' +import { Checkbox } from './Checkbox' +import { SearchInput } from '@/components/inputs/SearchInput' + +type Props = { + inputIndex: number + defaultItems: ChoiceInputBlock['items'] + options: ChoiceInputBlock['options'] + onSubmit: (value: InputSubmitContent) => void +} + +export const SearchableMultipleChoicesForm = (props: Props) => { + let inputRef: HTMLInputElement | undefined + const [filteredItems, setFilteredItems] = createSignal(props.defaultItems) + const [selectedItemIds, setSelectedItemIds] = createSignal([]) + + const handleClick = (itemId: string) => { + toggleSelectedItemId(itemId) + } + + const toggleSelectedItemId = (itemId: string) => { + const existingIndex = selectedItemIds().indexOf(itemId) + if (existingIndex !== -1) { + setSelectedItemIds((selectedItemIds) => + selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId) + ) + } else { + setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId]) + } + } + + const handleSubmit = () => + props.onSubmit({ + value: props.defaultItems + .filter((item) => selectedItemIds().includes(item.id)) + .join(', '), + }) + + const filterItems = (inputValue: string) => { + setFilteredItems( + props.defaultItems.filter((item) => + item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase()) + ) + ) + } + + return ( +
+
+ setFilteredItems(props.defaultItems)} + /> +
+
+ + {(item) => ( + +
selectedItemId === item.id + )} + on:click={() => handleClick(item.id)} + class={ + 'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' + + (selectedItemIds().some( + (selectedItemId) => selectedItemId === item.id + ) + ? ' selected' + : '') + } + data-itemid={item.id} + > +
+ selectedItemId === item.id + )} + /> + {item.content} +
+
+
+ )} +
+ + filteredItems().every((item) => item.id !== selectedItemId) + )} + > + {(selectedItemId) => ( + +
handleClick(selectedItemId)} + class={ + 'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected' + } + data-itemid={selectedItemId} + > +
+ + + { + props.defaultItems.find( + (item) => item.id === selectedItemId + )?.content + } + +
+
+
+ )} +
+
+ {selectedItemIds().length > 0 && ( + + {props.options?.buttonLabel ?? 'Send'} + + )} +
+ ) +} diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 18eb409911..daec988378 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.0.40", + "version": "0.0.41", "description": "React library to display typebots on your website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/schemas/features/blocks/inputs/choice.ts b/packages/schemas/features/blocks/inputs/choice.ts index 6402fa7a48..933e3f0acf 100644 --- a/packages/schemas/features/blocks/inputs/choice.ts +++ b/packages/schemas/features/blocks/inputs/choice.ts @@ -10,12 +10,14 @@ export const choiceInputOptionsSchema = optionBaseSchema.merge( isMultipleChoice: z.boolean(), buttonLabel: z.string(), dynamicVariableId: z.string().optional(), + isSearchable: z.boolean().optional(), }) ) export const defaultChoiceInputOptions: ChoiceInputOptions = { buttonLabel: defaultButtonLabel, isMultipleChoice: false, + isSearchable: false, } export const buttonItemSchema = itemBaseSchema.merge(