@ -12,13 +12,15 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
const handleIsMultipleChange = (isMultipleChoice: boolean) =>
|
const updateIsMultiple = (isMultipleChoice: boolean) =>
|
||||||
options && onOptionsChange({ ...options, isMultipleChoice })
|
options && onOptionsChange({ ...options, isMultipleChoice })
|
||||||
const handleButtonLabelChange = (buttonLabel: string) =>
|
const updateIsSearchable = (isSearchable: boolean) =>
|
||||||
|
options && onOptionsChange({ ...options, isSearchable })
|
||||||
|
const updateButtonLabel = (buttonLabel: string) =>
|
||||||
options && onOptionsChange({ ...options, buttonLabel })
|
options && onOptionsChange({ ...options, buttonLabel })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const updateSaveVariable = (variable?: Variable) =>
|
||||||
options && onOptionsChange({ ...options, variableId: variable?.id })
|
options && onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
const handleDynamicVariableChange = (variable?: Variable) =>
|
const updateDynamicDataVariable = (variable?: Variable) =>
|
||||||
options && onOptionsChange({ ...options, dynamicVariableId: variable?.id })
|
options && onOptionsChange({ ...options, dynamicVariableId: variable?.id })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,18 +28,23 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
label="Multiple choice?"
|
label="Multiple choice?"
|
||||||
initialValue={options?.isMultipleChoice ?? false}
|
initialValue={options?.isMultipleChoice ?? false}
|
||||||
onCheckChange={handleIsMultipleChange}
|
onCheckChange={updateIsMultiple}
|
||||||
|
/>
|
||||||
|
<SwitchWithLabel
|
||||||
|
label="Is searchable?"
|
||||||
|
initialValue={options?.isSearchable ?? false}
|
||||||
|
onCheckChange={updateIsSearchable}
|
||||||
/>
|
/>
|
||||||
{options?.isMultipleChoice && (
|
{options?.isMultipleChoice && (
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Button label:"
|
label="Button label:"
|
||||||
defaultValue={options?.buttonLabel ?? 'Send'}
|
defaultValue={options?.buttonLabel ?? 'Send'}
|
||||||
onChange={handleButtonLabelChange}
|
onChange={updateButtonLabel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Dynamic items from variable:{' '}
|
Dynamic data:{' '}
|
||||||
<MoreInfoTooltip>
|
<MoreInfoTooltip>
|
||||||
If defined, buttons will be dynamically displayed based on what the
|
If defined, buttons will be dynamically displayed based on what the
|
||||||
variable contains.
|
variable contains.
|
||||||
@ -45,7 +52,7 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<VariableSearchInput
|
<VariableSearchInput
|
||||||
initialVariableId={options?.dynamicVariableId}
|
initialVariableId={options?.dynamicVariableId}
|
||||||
onSelectVariable={handleDynamicVariableChange}
|
onSelectVariable={updateDynamicDataVariable}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -54,7 +61,7 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<VariableSearchInput
|
<VariableSearchInput
|
||||||
initialVariableId={options?.variableId}
|
initialVariableId={options?.variableId}
|
||||||
onSelectVariable={handleVariableChange}
|
onSelectVariable={updateSaveVariable}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -295,6 +295,54 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"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": {
|
"dynamicVariableId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isSearchable": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -1743,6 +1794,121 @@
|
|||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"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": {
|
"customSeatsLimit": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"isQuarantined": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -3093,7 +3262,8 @@
|
|||||||
"storageLimitSecondEmailSentAt",
|
"storageLimitSecondEmailSentAt",
|
||||||
"customChatsLimit",
|
"customChatsLimit",
|
||||||
"customStorageLimit",
|
"customStorageLimit",
|
||||||
"customSeatsLimit"
|
"customSeatsLimit",
|
||||||
|
"isQuarantined"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -3216,6 +3386,9 @@
|
|||||||
"customSeatsLimit": {
|
"customSeatsLimit": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"isQuarantined": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -3234,7 +3407,8 @@
|
|||||||
"storageLimitSecondEmailSentAt",
|
"storageLimitSecondEmailSentAt",
|
||||||
"customChatsLimit",
|
"customChatsLimit",
|
||||||
"customStorageLimit",
|
"customStorageLimit",
|
||||||
"customSeatsLimit"
|
"customSeatsLimit",
|
||||||
|
"isQuarantined"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -3374,6 +3548,9 @@
|
|||||||
"customSeatsLimit": {
|
"customSeatsLimit": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"isQuarantined": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -3392,7 +3569,8 @@
|
|||||||
"storageLimitSecondEmailSentAt",
|
"storageLimitSecondEmailSentAt",
|
||||||
"customChatsLimit",
|
"customChatsLimit",
|
||||||
"customStorageLimit",
|
"customStorageLimit",
|
||||||
"customSeatsLimit"
|
"customSeatsLimit",
|
||||||
|
"isQuarantined"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -4642,6 +4820,9 @@
|
|||||||
"customSeatsLimit": {
|
"customSeatsLimit": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"isQuarantined": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -4660,7 +4841,8 @@
|
|||||||
"storageLimitSecondEmailSentAt",
|
"storageLimitSecondEmailSentAt",
|
||||||
"customChatsLimit",
|
"customChatsLimit",
|
||||||
"customStorageLimit",
|
"customStorageLimit",
|
||||||
"customSeatsLimit"
|
"customSeatsLimit",
|
||||||
|
"isQuarantined"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -459,6 +459,9 @@
|
|||||||
},
|
},
|
||||||
"dynamicVariableId": {
|
"dynamicVariableId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isSearchable": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -1404,6 +1407,121 @@
|
|||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"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": {
|
"dynamicVariableId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isSearchable": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.40",
|
"version": "0.0.41",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -286,3 +286,14 @@ textarea {
|
|||||||
.typebot-popup-blocked-toast {
|
.typebot-popup-blocked-toast {
|
||||||
border-radius: var(--typebot-border-radius);
|
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 */
|
||||||
|
}
|
||||||
|
@ -47,38 +47,42 @@ export const ChatChunk = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full min-w-0 gap-2">
|
<div class="flex flex-col w-full min-w-0 gap-2">
|
||||||
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
|
<Show when={props.messages.length > 0}>
|
||||||
<Show
|
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
|
||||||
when={
|
<Show
|
||||||
props.theme.chat.hostAvatar?.isEnabled && props.messages.length > 0
|
when={
|
||||||
}
|
props.theme.chat.hostAvatar?.isEnabled &&
|
||||||
>
|
props.messages.length > 0
|
||||||
<AvatarSideContainer
|
}
|
||||||
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
|
>
|
||||||
hideAvatar={props.hideAvatar}
|
<AvatarSideContainer
|
||||||
/>
|
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
|
||||||
</Show>
|
hideAvatar={props.hideAvatar}
|
||||||
<div
|
/>
|
||||||
class="flex flex-col flex-1 gap-2"
|
</Show>
|
||||||
style={{
|
|
||||||
'margin-right': props.theme.chat.guestAvatar?.isEnabled
|
<div
|
||||||
? isMobile()
|
class="flex flex-col flex-1 gap-2"
|
||||||
? '32px'
|
style={{
|
||||||
: '48px'
|
'margin-right': props.theme.chat.guestAvatar?.isEnabled
|
||||||
: undefined,
|
? isMobile()
|
||||||
}}
|
? '32px'
|
||||||
>
|
: '48px'
|
||||||
<For each={props.messages.slice(0, displayedMessageIndex() + 1)}>
|
: undefined,
|
||||||
{(message) => (
|
}}
|
||||||
<HostBubble
|
>
|
||||||
message={message}
|
<For each={props.messages.slice(0, displayedMessageIndex() + 1)}>
|
||||||
typingEmulation={props.settings.typingEmulation}
|
{(message) => (
|
||||||
onTransitionEnd={displayNextMessage}
|
<HostBubble
|
||||||
/>
|
message={message}
|
||||||
)}
|
typingEmulation={props.settings.typingEmulation}
|
||||||
</For>
|
onTransitionEnd={displayNextMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
{props.input && displayedMessageIndex() === props.messages.length && (
|
{props.input && displayedMessageIndex() === props.messages.length && (
|
||||||
<InputChatBlock
|
<InputChatBlock
|
||||||
block={props.input}
|
block={props.input}
|
||||||
|
@ -30,6 +30,8 @@ import { isMobile } from '@/utils/isMobileSignal'
|
|||||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||||
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
||||||
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
||||||
|
import { SearchableButtons } from '@/features/blocks/inputs/buttons/components/SearchableButtons'
|
||||||
|
import { SearchableMultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: NonNullable<ChatReply['input']>
|
block: NonNullable<ChatReply['input']>
|
||||||
@ -159,30 +161,49 @@ const Input = (props: {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match when={isButtonsBlock(props.block)} keyed>
|
||||||
when={
|
{(block) => (
|
||||||
props.block.type === InputBlockType.CHOICE &&
|
<Switch>
|
||||||
props.block.options.isMultipleChoice
|
<Match when={!block.options.isMultipleChoice}>
|
||||||
}
|
<Switch>
|
||||||
>
|
<Match when={block.options.isSearchable}>
|
||||||
<MultipleChoicesForm
|
<SearchableButtons
|
||||||
inputIndex={props.inputIndex}
|
inputIndex={props.inputIndex}
|
||||||
items={(props.block as ChoiceInputBlock).items}
|
defaultItems={block.items}
|
||||||
options={props.block.options as ChoiceInputBlock['options']}
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
/>
|
||||||
/>
|
</Match>
|
||||||
</Match>
|
<Match when={!block.options.isSearchable}>
|
||||||
<Match
|
<Buttons
|
||||||
when={
|
inputIndex={props.inputIndex}
|
||||||
props.block.type === InputBlockType.CHOICE &&
|
items={block.items}
|
||||||
!props.block.options.isMultipleChoice
|
onSubmit={onSubmit}
|
||||||
}
|
/>
|
||||||
>
|
</Match>
|
||||||
<Buttons
|
</Switch>
|
||||||
inputIndex={props.inputIndex}
|
</Match>
|
||||||
items={(props.block as ChoiceInputBlock).items}
|
<Match when={block.options.isMultipleChoice}>
|
||||||
onSubmit={onSubmit}
|
<Switch>
|
||||||
/>
|
<Match when={block.options.isSearchable}>
|
||||||
|
<SearchableMultipleChoicesForm
|
||||||
|
inputIndex={props.inputIndex}
|
||||||
|
defaultItems={block.items}
|
||||||
|
options={block.options}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!block.options.isSearchable}>
|
||||||
|
<MultipleChoicesForm
|
||||||
|
inputIndex={props.inputIndex}
|
||||||
|
items={block.items}
|
||||||
|
options={block.options}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.RATING}>
|
<Match when={props.block.type === InputBlockType.RATING}>
|
||||||
<RatingForm
|
<RatingForm
|
||||||
@ -214,3 +235,8 @@ const Input = (props: {
|
|||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isButtonsBlock = (
|
||||||
|
block: ChatReply['input']
|
||||||
|
): ChoiceInputBlock | undefined =>
|
||||||
|
block?.type === InputBlockType.CHOICE ? block : undefined
|
||||||
|
17
packages/embeds/js/src/components/icons/CloseIcon.tsx
Normal file
17
packages/embeds/js/src/components/icons/CloseIcon.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const CloseIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
)
|
43
packages/embeds/js/src/components/inputs/SearchInput.tsx
Normal file
43
packages/embeds/js/src/components/inputs/SearchInput.tsx
Normal file
@ -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<JSX.InputHTMLAttributes<HTMLInputElement>, '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 (
|
||||||
|
<div class="flex justify-between items-center gap-2 w-full pr-4">
|
||||||
|
<input
|
||||||
|
ref={props.ref}
|
||||||
|
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||||
|
type="text"
|
||||||
|
style={{ 'font-size': '16px' }}
|
||||||
|
value={value()}
|
||||||
|
onInput={(e) => changeValue(e.currentTarget.value)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
<Show when={value().length > 0}>
|
||||||
|
<button class="w-5 h-5" on:click={clearValue}>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -72,13 +72,11 @@ export const MultipleChoicesForm = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
{selectedIndices().length > 0 && (
|
||||||
{selectedIndices().length > 0 && (
|
<SendButton disableIcon>
|
||||||
<SendButton disableIcon>
|
{props.options?.buttonLabel ?? 'Send'}
|
||||||
{props.options?.buttonLabel ?? 'Send'}
|
</SendButton>
|
||||||
</SendButton>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
<div class="flex flex-col gap-2 w-full">
|
||||||
|
<div class="flex items-end typebot-input w-full">
|
||||||
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={filterItems}
|
||||||
|
placeholder="Filter the options..."
|
||||||
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item, index) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<Button
|
||||||
|
on:click={handleClick(index())}
|
||||||
|
data-itemid={item.id}
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</Button>
|
||||||
|
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
||||||
|
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||||
|
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -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<string[]>([])
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
|
||||||
|
<div class="flex items-end typebot-input w-full">
|
||||||
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={filterItems}
|
||||||
|
placeholder="Filter the options..."
|
||||||
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selectedItemIds().some(
|
||||||
|
(selectedItemId) => 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}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedItemIds().some(
|
||||||
|
(selectedItemId) => selectedItemId === item.id
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>{item.content}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<For
|
||||||
|
each={selectedItemIds().filter((selectedItemId) =>
|
||||||
|
filteredItems().every((item) => item.id !== selectedItemId)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(selectedItemId) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked
|
||||||
|
on:click={() => handleClick(selectedItemId)}
|
||||||
|
class={
|
||||||
|
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected'
|
||||||
|
}
|
||||||
|
data-itemid={selectedItemId}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox isChecked />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
props.defaultItems.find(
|
||||||
|
(item) => item.id === selectedItemId
|
||||||
|
)?.content
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
{selectedItemIds().length > 0 && (
|
||||||
|
<SendButton disableIcon>
|
||||||
|
{props.options?.buttonLabel ?? 'Send'}
|
||||||
|
</SendButton>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.0.40",
|
"version": "0.0.41",
|
||||||
"description": "React library to display typebots on your website",
|
"description": "React library to display typebots on your website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -10,12 +10,14 @@ export const choiceInputOptionsSchema = optionBaseSchema.merge(
|
|||||||
isMultipleChoice: z.boolean(),
|
isMultipleChoice: z.boolean(),
|
||||||
buttonLabel: z.string(),
|
buttonLabel: z.string(),
|
||||||
dynamicVariableId: z.string().optional(),
|
dynamicVariableId: z.string().optional(),
|
||||||
|
isSearchable: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
||||||
buttonLabel: defaultButtonLabel,
|
buttonLabel: defaultButtonLabel,
|
||||||
isMultipleChoice: false,
|
isMultipleChoice: false,
|
||||||
|
isSearchable: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buttonItemSchema = itemBaseSchema.merge(
|
export const buttonItemSchema = itemBaseSchema.merge(
|
||||||
|
Reference in New Issue
Block a user