2
0

(engine) Improve engine overall robustness

This commit is contained in:
Baptiste Arnaud
2023-01-25 11:27:47 +01:00
parent ff62b922a0
commit 30baa611e5
210 changed files with 1820 additions and 1919 deletions

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { AudioBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { AudioBubbleContent } from 'models'
import { createSignal, onCleanup, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { EmbedBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { EmbedBubbleContent } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { ImageBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { ImageBubbleContent } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { TextBubbleContent, TypingEmulation } from 'models'
import { TypingBubble } from '@/components'
import type { TextBubbleContent, TypingEmulation } from 'models'
import { createSignal, onMount } from 'solid-js'
import { computeTypingDuration } from '../utils/computeTypingDuration'

View File

@ -1,4 +1,4 @@
import { TypingEmulation } from 'models'
import type { TypingEmulation } from 'models'
export const computeTypingDuration = (
bubbleContent: string,

View File

@ -1,5 +1,6 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { VideoBubbleContent, VideoBubbleContentType } from 'models'
import { TypingBubble } from '@/components'
import type { VideoBubbleContent } from 'models'
import { VideoBubbleContentType } from 'models/features/blocks/bubbles/video/enums'
import { createSignal, Match, onMount, Switch } from 'solid-js'
type Props = {
@ -64,10 +65,7 @@ const VideoContent = (props: VideoContentProps) => {
<Match
when={
props.content?.type &&
[
VideoBubbleContentType.VIMEO,
VideoBubbleContentType.YOUTUBE,
].includes(props.content.type)
props.content.type === VideoBubbleContentType.URL
}
>
<video

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { ChoiceInputBlock } from 'models'
import type { ChoiceInputBlock } from 'models'
import { createSignal, For } from 'solid-js'
type Props = {
@ -12,8 +12,7 @@ type Props = {
export const ChoiceForm = (props: Props) => {
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
const handleClick = (itemIndex: number) => (e: MouseEvent) => {
e.preventDefault()
const handleClick = (itemIndex: number) => {
if (props.block.options?.isMultipleChoice)
toggleSelectedItemIndex(itemIndex)
else props.onSubmit({ value: props.block.items[itemIndex].content ?? '' })
@ -47,7 +46,8 @@ export const ChoiceForm = (props: Props) => {
role={
props.block.options?.isMultipleChoice ? 'checkbox' : 'button'
}
onClick={(event) => handleClick(index())(event)}
type="button"
on:click={() => handleClick(index())}
class={
'py-2 px-4 text-left font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
(selectedIndices().some(

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { DateInputOptions } from 'models'
import type { DateInputOptions } from 'models'
import { createSignal } from 'solid-js'
import { parseReadableDate } from '../utils/parseReadableDate'

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { EmailInputBlock } from 'models'
import type { EmailInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -59,7 +59,7 @@ export const EmailInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,6 +1,7 @@
import { SendButton, Spinner } from '@/components/SendButton'
import { BotContext, InputSubmitContent } from '@/types'
import { defaultFileInputOptions, FileInputBlock } from 'models'
import { FileInputBlock } from 'models'
import { defaultFileInputOptions } from 'models/features/blocks/inputs/file'
import { createSignal, Match, Show, Switch } from 'solid-js'
import { uploadFiles } from 'utils'
@ -140,7 +141,7 @@ export const FileUploadForm = (props: Props) => {
<span class="relative">
<FileIcon />
<div
class="total-files-indicator flex items-center justify-center absolute -right-1 rounded-full px-1 h-4"
class="total-files-indicator flex items-center justify-center absolute -right-1 rounded-full px-1 w-4 h-4"
style={{ bottom: '5px' }}
>
{selectedFiles().length}
@ -177,7 +178,7 @@ export const FileUploadForm = (props: Props) => {
class={
'py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button '
}
onClick={() => props.onSkip()}
on:click={() => props.onSkip()}
>
{props.block.options.labels.skip ??
defaultFileInputOptions.labels.skip}
@ -198,7 +199,7 @@ export const FileUploadForm = (props: Props) => {
class={
'secondary-button py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 mr-2'
}
onClick={clearFiles}
on:click={clearFiles}
>
{props.block.options.labels.clear ??
defaultFileInputOptions.labels.clear}
@ -233,7 +234,7 @@ const UploadIcon = () => (
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-3"
class="mb-3 text-gray-500"
>
<polyline points="16 16 12 12 8 16" />
<line x1="12" y1="12" x2="12" y2="21" />
@ -244,7 +245,6 @@ const UploadIcon = () => (
const FileIcon = () => (
<svg
class="mb-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
@ -254,6 +254,7 @@ const FileIcon = () => (
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-3 text-gray-500"
>
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
<polyline points="13 2 13 9 20 9" />

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { NumberInputBlock } from 'models'
import type { NumberInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type NumberInputProps = {
@ -62,7 +62,7 @@ export const NumberInput = (props: NumberInputProps) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,5 +1,6 @@
import { BotContext } from '@/types'
import { PaymentInputOptions, PaymentProvider, RuntimeOptions } from 'models'
import type { PaymentInputOptions, RuntimeOptions } from 'models'
import { PaymentProvider } from 'models/features/blocks/inputs/payment/enums'
import { Match, Switch } from 'solid-js'
import { StripePaymentForm } from './StripePaymentForm'

View File

@ -1,9 +1,9 @@
import { SendButton } from '@/components/SendButton'
import { createSignal, onMount, Show } from 'solid-js'
import { loadStripe } from '@stripe/stripe-js/pure'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import { BotContext } from '@/types'
import { PaymentInputOptions, RuntimeOptions } from 'models'
import type { PaymentInputOptions, RuntimeOptions } from 'models'
import { loadStripe } from '@/lib/stripe'
type Props = {
context: BotContext

View File

@ -1,4 +1,4 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
@ -99,7 +99,7 @@ export const PhoneInput = (props: PhoneInputProps) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { RatingInputBlock, RatingInputOptions } from 'models'
import type { RatingInputBlock, RatingInputOptions } from 'models'
import { createSignal, For, Match, Switch } from 'solid-js'
import { isDefined, isEmpty, isNotDefined } from 'utils'
@ -84,7 +84,7 @@ const RatingButton = (props: RatingButtonProps) => {
<Switch>
<Match when={props.buttonType === 'Numbers'}>
<button
onClick={(e) => {
on:click={(e) => {
e.preventDefault()
props.onClick(props.idx)
}}
@ -111,7 +111,7 @@ const RatingButton = (props: RatingButtonProps) => {
? props.customIcon.svg
: defaultIcon
}
onClick={() => props.onClick(props.idx)}
on:click={() => props.onClick(props.idx)}
/>
</Match>
</Switch>

View File

@ -1,8 +1,8 @@
import { Textarea, ShortTextInput } from '@/components/inputs'
import { Textarea, ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { TextInputBlock } from 'models'
import type { TextInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -69,7 +69,7 @@ export const TextInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { UrlInputBlock } from 'models'
import type { UrlInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -65,7 +65,7 @@ export const UrlInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,5 +1,5 @@
import { executeCode } from '@/features/blocks/logic/code'
import { CodeToExecute } from 'models'
import type { CodeToExecute } from 'models'
export const executeChatwoot = (chatwoot: { codeToExecute: CodeToExecute }) => {
executeCode(chatwoot.codeToExecute)

View File

@ -1,5 +1,5 @@
import { sendGaEvent } from '@/lib/gtag'
import { GoogleAnalyticsOptions } from 'models'
import type { GoogleAnalyticsOptions } from 'models'
export const executeGoogleAnalyticsBlock = async (
options: GoogleAnalyticsOptions

View File

@ -1,4 +1,4 @@
import { CodeToExecute } from 'models'
import type { CodeToExecute } from 'models'
export const executeCode = async ({ content, args }: CodeToExecute) => {
const func = Function(...args.map((arg) => arg.id), content)

View File

@ -1,4 +1,4 @@
import { RedirectOptions } from 'models'
import type { RedirectOptions } from 'models'
export const executeRedirect = ({ url, isNewTab }: RedirectOptions) => {
if (!url) return

View File

@ -1,11 +1,11 @@
import styles from '../../../assets/index.css'
import { createSignal, onMount, Show, splitProps, onCleanup } from 'solid-js'
import { Bot, BotProps } from '../../../components/Bot'
import { CommandData } from '@/features/commands'
import styles from '../../../assets/index.css'
import { CommandData } from '../../commands'
import { BubbleButton } from './BubbleButton'
import { PreviewMessage, PreviewMessageProps } from './PreviewMessage'
import { isDefined } from 'utils'
import { BubbleParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
export type BubbleProps = BotProps &
BubbleParams & {
@ -131,7 +131,11 @@ export const Bubble = (props: BubbleProps) => {
}
>
<Show when={isBotStarted()}>
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
<Bot
{...botProps}
prefilledVariables={prefilledVariables()}
class="rounded-lg"
/>
</Show>
</div>
</>

View File

@ -1,4 +1,5 @@
import { Show } from 'solid-js'
import { isNotDefined } from 'utils'
import { ButtonTheme } from '../types'
type Props = ButtonTheme & {
@ -7,6 +8,7 @@ type Props = ButtonTheme & {
}
const defaultButtonColor = '#0042DA'
const defaultIconColor = 'white'
export const BubbleButton = (props: Props) => {
return (
@ -20,27 +22,23 @@ export const BubbleButton = (props: Props) => {
'background-color': props.backgroundColor ?? defaultButtonColor,
}}
>
<Show when={props.icon?.color} keyed>
{(color) => (
<svg
viewBox="0 0 24 24"
style={{
stroke: color,
}}
class={
`w-7 stroke-2 fill-transparent absolute duration-200 transition ` +
(props.isBotOpened
? 'scale-0 opacity-0'
: 'scale-100 opacity-100')
}
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
)}
<Show when={isNotDefined(props.customIconSrc)} keyed>
<svg
viewBox="0 0 24 24"
style={{
stroke: props.iconColor ?? defaultIconColor,
}}
class={
`w-7 stroke-2 fill-transparent absolute duration-200 transition ` +
(props.isBotOpened ? 'scale-0 opacity-0' : 'scale-100 opacity-100')
}
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
</Show>
<Show when={props.icon?.url}>
<Show when={props.customIconSrc}>
<img
src={props.icon?.url}
src={props.customIconSrc}
class="w-7 h-7 rounded-full object-cover"
alt="Bubble button icon"
/>
@ -48,7 +46,7 @@ export const BubbleButton = (props: Props) => {
<svg
viewBox="0 0 24 24"
style={{ fill: props.icon?.color ?? 'white' }}
style={{ fill: props.iconColor ?? 'white' }}
class={
`w-7 absolute duration-200 transition ` +
(props.isBotOpened

View File

@ -10,8 +10,8 @@ export type PreviewMessageProps = Pick<
onCloseClick: () => void
}
const defaultFontFamily =
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
const defaultBackgroundColor = '#F7F8FF'
const defaultTextColor = '#303235'
export const PreviewMessage = (props: PreviewMessageProps) => {
const [isPreviewMessageHovered, setIsPreviewMessageHovered] =
@ -23,11 +23,9 @@ 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.previewMessageTheme?.fontFamily ?? defaultFontFamily,
'background-color':
props.previewMessageTheme?.backgroundColor ?? '#F7F8FF',
color: props.previewMessageTheme?.color ?? '#303235',
props.previewMessageTheme?.backgroundColor ?? defaultBackgroundColor,
color: props.previewMessageTheme?.textColor ?? defaultTextColor,
}}
onMouseEnter={() => setIsPreviewMessageHovered(true)}
onMouseLeave={() => setIsPreviewMessageHovered(false)}
@ -43,8 +41,10 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
}}
style={{
'background-color':
props.previewMessageTheme?.closeButtonBgColor ?? '#F7F8FF',
color: props.previewMessageTheme?.closeButtonColor ?? '#303235',
props.previewMessageTheme?.closeButtonBackgroundColor ??
defaultBackgroundColor,
color:
props.previewMessageTheme?.closeButtonIconColor ?? defaultTextColor,
}}
>
<svg

View File

@ -10,10 +10,8 @@ export type BubbleTheme = {
export type ButtonTheme = {
backgroundColor?: string
icon?: {
color?: string
url?: string
}
iconColor?: string
customIconSrc?: string
}
export type PreviewMessageParams = {
@ -24,8 +22,7 @@ export type PreviewMessageParams = {
export type PreviewMessageTheme = {
backgroundColor?: string
color?: string
fontFamily?: string
closeButtonBgColor?: string
closeButtonColor?: string
textColor?: string
closeButtonBackgroundColor?: string
closeButtonIconColor?: string
}

View File

@ -7,10 +7,10 @@ import {
onCleanup,
createEffect,
} from 'solid-js'
import { Bot, BotProps } from '../../../components/Bot'
import { CommandData } from '@/features/commands'
import { isDefined } from 'utils'
import { CommandData } from '../../commands'
import { isDefined, isNotDefined } from 'utils'
import { PopupParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
export type PopupProps = BotProps &
PopupParams & {
@ -43,8 +43,6 @@ export const Popup = (props: PopupProps) => {
)
onMount(() => {
document.addEventListener('pointerdown', processWindowClick)
botContainer?.addEventListener('pointerdown', stopPropagation)
window.addEventListener('message', processIncomingEvent)
const autoShowDelay = popupProps.autoShowDelay
if (isDefined(autoShowDelay)) {
@ -54,20 +52,14 @@ export const Popup = (props: PopupProps) => {
}
})
createEffect(() => {
const isOpen = popupProps.isOpen
if (isDefined(isOpen)) setIsBotOpened(isOpen)
})
onCleanup(() => {
document.removeEventListener('pointerdown', processWindowClick)
botContainer?.removeEventListener('pointerdown', stopPropagation)
window.removeEventListener('message', processIncomingEvent)
})
const processWindowClick = () => {
setIsBotOpened(false)
}
createEffect(() => {
if (isNotDefined(props.isOpen) || props.isOpen === isBotOpened()) return
toggleBot()
})
const stopPropagation = (event: MouseEvent) => {
event.stopPropagation()
@ -87,24 +79,28 @@ export const Popup = (props: PopupProps) => {
}
const openBot = () => {
if (isBotOpened()) popupProps.onOpen?.()
if (isDefined(props.isOpen)) return
setIsBotOpened(true)
popupProps.onOpen?.()
document.body.style.overflow = 'hidden'
document.addEventListener('pointerdown', closeBot)
botContainer?.addEventListener('pointerdown', stopPropagation)
}
const closeBot = () => {
if (isBotOpened()) popupProps.onClose?.()
if (isDefined(props.isOpen)) return
setIsBotOpened(false)
popupProps.onClose?.()
document.body.style.overflow = 'auto'
document.removeEventListener('pointerdown', closeBot)
botContainer?.removeEventListener('pointerdown', stopPropagation)
}
const toggleBot = () => {
if (isDefined(props.isOpen)) return
isBotOpened() ? closeBot() : openBot()
}
return (
<Show when={isBotOpened()}>
<style>{styles}</style>
<div
class="relative z-10"
aria-labelledby="modal-title"

View File

@ -2,6 +2,5 @@ export type PopupParams = {
autoShowDelay?: number
theme?: {
width?: string
backgroundColor?: string
}
}

View File

@ -0,0 +1,47 @@
import styles from '../../../assets/index.css'
import { Bot, BotProps } from '@/components/Bot'
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
const hostElementCss = `
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: hidden;
}
`
export const Standard = (props: BotProps) => {
const [isBotDisplayed, setIsBotDisplayed] = createSignal(false)
const launchBot = () => {
setIsBotDisplayed(true)
}
const observer = new IntersectionObserver((intersections) => {
if (intersections.some((intersection) => intersection.isIntersecting))
launchBot()
})
onMount(() => {
const standardElement = document.querySelector('typebot-standard')
if (!standardElement) return
observer.observe(standardElement)
})
onCleanup(() => {
observer.disconnect()
})
return (
<>
<style>
{styles}
{hostElementCss}
</style>
<Show when={isBotDisplayed()}>
<Bot {...props} />
</Show>
</>
)
}

View File

@ -0,0 +1 @@
export * from './Standard'

View File

@ -0,0 +1 @@
export * from './components'