♻️ (bot) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
a5c8a8a95c
commit
972094425a
4
apps/builder/src/test/utils/playwright.ts
Normal file
4
apps/builder/src/test/utils/playwright.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const getTestAsset = (name: string) =>
|
||||||
|
path.join(__dirname, '..', 'assets', name)
|
@ -30,5 +30,19 @@ module.exports = {
|
|||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'react/display-name': [0],
|
'react/display-name': [0],
|
||||||
'@next/next/no-img-element': [0],
|
'@next/next/no-img-element': [0],
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
'*/src/*',
|
||||||
|
'src/*',
|
||||||
|
'*/src',
|
||||||
|
'@/features/*/*',
|
||||||
|
'@/index',
|
||||||
|
'!@/features/blocks/*',
|
||||||
|
'!@/features/*/api',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
import { useAnswers } from '../../../providers/AnswersProvider'
|
||||||
import { InputBlock, InputBlockType } from 'models'
|
import { InputBlock, InputBlockType } from 'models'
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { TextForm } from './inputs/TextForm'
|
|
||||||
import { byId } from 'utils'
|
import { byId } from 'utils'
|
||||||
import { DateForm } from './inputs/DateForm'
|
import { InputSubmitContent } from '@/types'
|
||||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { isInputValid } from '@/utils/inputs'
|
||||||
import { parseVariables } from '../../../services/variable'
|
import { parseVariables } from '@/features/variables'
|
||||||
import { isInputValid } from 'services/inputs'
|
import { TextInput } from '@/features/blocks/inputs/textInput'
|
||||||
import { PaymentForm } from './inputs/PaymentForm'
|
import { NumberInput } from '@/features/blocks/inputs/number'
|
||||||
import { RatingForm } from './inputs/RatingForm'
|
import { EmailInput } from '@/features/blocks/inputs/email'
|
||||||
import { FileUploadForm } from './inputs/FileUploadForm'
|
import { UrlInput } from '@/features/blocks/inputs/url'
|
||||||
|
import { PhoneInput } from '@/features/blocks/inputs/phone'
|
||||||
export type InputSubmitContent = {
|
import { DateForm } from '@/features/blocks/inputs/date'
|
||||||
label?: string
|
import { ChoiceForm } from '@/features/blocks/inputs/buttons'
|
||||||
value: string
|
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||||
itemId?: string
|
import { RatingForm } from '@/features/blocks/inputs/rating'
|
||||||
}
|
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
|
||||||
|
|
||||||
export const InputChatBlock = ({
|
export const InputChatBlock = ({
|
||||||
block,
|
block,
|
||||||
@ -111,12 +110,44 @@ const Input = ({
|
|||||||
}) => {
|
}) => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case InputBlockType.TEXT:
|
case InputBlockType.TEXT:
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
block={block}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case InputBlockType.NUMBER:
|
case InputBlockType.NUMBER:
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
block={block}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case InputBlockType.EMAIL:
|
case InputBlockType.EMAIL:
|
||||||
|
return (
|
||||||
|
<EmailInput
|
||||||
|
block={block}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case InputBlockType.URL:
|
case InputBlockType.URL:
|
||||||
|
return (
|
||||||
|
<UrlInput
|
||||||
|
block={block}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
hasGuestAvatar={hasGuestAvatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case InputBlockType.PHONE:
|
case InputBlockType.PHONE:
|
||||||
return (
|
return (
|
||||||
<TextForm
|
<PhoneInput
|
||||||
block={block}
|
block={block}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Avatar } from 'components/avatars/Avatar'
|
import { Avatar } from '@/components/avatars/Avatar'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { CSSTransition } from 'react-transition-group'
|
import { CSSTransition } from 'react-transition-group'
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { EmbedBubble } from '@/features/blocks/bubbles/embed'
|
||||||
|
import { ImageBubble } from '@/features/blocks/bubbles/image'
|
||||||
|
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
|
||||||
|
import { VideoBubble } from '@/features/blocks/bubbles/video'
|
||||||
import { BubbleBlock, BubbleBlockType } from 'models'
|
import { BubbleBlock, BubbleBlockType } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { EmbedBubble } from './EmbedBubble'
|
|
||||||
import { ImageBubble } from './ImageBubble'
|
|
||||||
import { TextBubble } from './TextBubble'
|
|
||||||
import { VideoBubble } from './VideoBubble'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: BubbleBlock
|
block: BubbleBlock
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
import {
|
|
||||||
TextInputBlock,
|
|
||||||
EmailInputBlock,
|
|
||||||
NumberInputBlock,
|
|
||||||
InputBlockType,
|
|
||||||
UrlInputBlock,
|
|
||||||
PhoneNumberInputBlock,
|
|
||||||
} from 'models'
|
|
||||||
import React, { ChangeEvent, ChangeEventHandler } from 'react'
|
|
||||||
import PhoneInput, { Value, Country } from 'react-phone-number-input'
|
|
||||||
import { isMobile } from 'services/utils'
|
|
||||||
|
|
||||||
type TextInputProps = {
|
|
||||||
inputRef: React.RefObject<any>
|
|
||||||
block:
|
|
||||||
| TextInputBlock
|
|
||||||
| EmailInputBlock
|
|
||||||
| NumberInputBlock
|
|
||||||
| UrlInputBlock
|
|
||||||
| PhoneNumberInputBlock
|
|
||||||
value: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextInput = ({
|
|
||||||
inputRef,
|
|
||||||
block,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: TextInputProps) => {
|
|
||||||
const handleInputChange = (
|
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
||||||
) => onChange(e.target.value)
|
|
||||||
|
|
||||||
const handlePhoneNumberChange = (value?: Value | undefined) => {
|
|
||||||
onChange(value as string)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (block.type) {
|
|
||||||
case InputBlockType.TEXT: {
|
|
||||||
return block.options?.isLong ? (
|
|
||||||
<LongTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
placeholder={
|
|
||||||
block.options?.labels?.placeholder ?? 'Type your answer...'
|
|
||||||
}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ShortTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
placeholder={
|
|
||||||
block.options?.labels?.placeholder ?? 'Type your answer...'
|
|
||||||
}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
// Hack to disable Chrome autocomplete
|
|
||||||
name="no-name"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case InputBlockType.EMAIL: {
|
|
||||||
return (
|
|
||||||
<ShortTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
placeholder={
|
|
||||||
block.options?.labels?.placeholder ?? 'Type your email...'
|
|
||||||
}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case InputBlockType.NUMBER: {
|
|
||||||
return (
|
|
||||||
<ShortTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
placeholder={
|
|
||||||
block.options?.labels?.placeholder ?? 'Type your answer...'
|
|
||||||
}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
type="number"
|
|
||||||
style={{ appearance: 'auto' }}
|
|
||||||
min={block.options?.min}
|
|
||||||
max={block.options?.max}
|
|
||||||
step={block.options?.step ?? 'any'}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case InputBlockType.URL: {
|
|
||||||
return (
|
|
||||||
<ShortTextInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
placeholder={block.options?.labels?.placeholder ?? 'Type your URL...'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
type="url"
|
|
||||||
autoComplete="url"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case InputBlockType.PHONE: {
|
|
||||||
return (
|
|
||||||
<PhoneInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={value}
|
|
||||||
onChange={handlePhoneNumberChange}
|
|
||||||
placeholder={
|
|
||||||
block.options.labels.placeholder ?? 'Your phone number...'
|
|
||||||
}
|
|
||||||
defaultCountry={block.options.defaultCountryCode as Country}
|
|
||||||
autoFocus={!isMobile}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShortTextInput = React.forwardRef(
|
|
||||||
(
|
|
||||||
props: React.InputHTMLAttributes<HTMLInputElement>,
|
|
||||||
ref: React.ForwardedRef<HTMLInputElement>
|
|
||||||
) => (
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
|
||||||
type="text"
|
|
||||||
style={{ fontSize: '16px' }}
|
|
||||||
autoFocus={!isMobile}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const LongTextInput = React.forwardRef(
|
|
||||||
(
|
|
||||||
props: {
|
|
||||||
placeholder: string
|
|
||||||
value: string
|
|
||||||
onChange: ChangeEventHandler
|
|
||||||
},
|
|
||||||
ref: React.ForwardedRef<HTMLTextAreaElement>
|
|
||||||
) => (
|
|
||||||
<textarea
|
|
||||||
ref={ref}
|
|
||||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
|
||||||
rows={6}
|
|
||||||
data-testid="textarea"
|
|
||||||
required
|
|
||||||
style={{ fontSize: '16px' }}
|
|
||||||
autoFocus={!isMobile}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)
|
|
@ -1 +0,0 @@
|
|||||||
export { TextForm } from './TextForm'
|
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||||
import { LinkedTypebot, useTypebot } from '../../contexts/TypebotContext'
|
import { LinkedTypebot, useTypebot } from '../../providers/TypebotProvider'
|
||||||
import {
|
import {
|
||||||
isBubbleBlock,
|
isBubbleBlock,
|
||||||
isBubbleBlockType,
|
isBubbleBlockType,
|
||||||
@ -12,11 +12,6 @@ import {
|
|||||||
isLogicBlock,
|
isLogicBlock,
|
||||||
byId,
|
byId,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { executeLogic } from 'services/logic'
|
|
||||||
import { executeIntegration } from 'services/integration'
|
|
||||||
import { parseRetryBlock, blockCanBeRetried } from 'services/inputs'
|
|
||||||
import { parseVariables } from '../../services/variable'
|
|
||||||
import { useAnswers } from 'contexts/AnswersContext'
|
|
||||||
import {
|
import {
|
||||||
BubbleBlock,
|
BubbleBlock,
|
||||||
InputBlock,
|
InputBlock,
|
||||||
@ -24,10 +19,16 @@ import {
|
|||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
Block,
|
Block,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { useChat } from 'contexts/ChatContext'
|
|
||||||
import { getLastChatBlockType } from 'services/chat'
|
|
||||||
import { HostBubble } from './ChatBlock/bubbles/HostBubble'
|
import { HostBubble } from './ChatBlock/bubbles/HostBubble'
|
||||||
import { InputChatBlock, InputSubmitContent } from './ChatBlock/InputChatBlock'
|
import { InputChatBlock } from './ChatBlock/InputChatBlock'
|
||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { useAnswers } from '@/providers/AnswersProvider'
|
||||||
|
import { useChat } from '@/providers/ChatProvider'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { getLastChatBlockType } from '@/utils/chat'
|
||||||
|
import { executeIntegration } from '@/utils/executeIntegration'
|
||||||
|
import { executeLogic } from '@/utils/executeLogic'
|
||||||
|
import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs'
|
||||||
|
|
||||||
type ChatGroupProps = {
|
type ChatGroupProps = {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { ChatGroup } from './ChatGroup'
|
import { ChatGroup } from './ChatGroup'
|
||||||
import { useFrame } from 'react-frame-component'
|
import { useFrame } from 'react-frame-component'
|
||||||
import { setCssVariablesValue } from '../services/theme'
|
import { useAnswers } from '../providers/AnswersProvider'
|
||||||
import { useAnswers } from '../contexts/AnswersContext'
|
|
||||||
import { Group, Edge, PublicTypebot, Theme, VariableWithValue } from 'models'
|
import { Group, Edge, PublicTypebot, Theme, VariableWithValue } from 'models'
|
||||||
import { byId, isDefined, isInputBlock, isNotDefined } from 'utils'
|
import { byId, isDefined, isInputBlock, isNotDefined } from 'utils'
|
||||||
import { animateScroll as scroll } from 'react-scroll'
|
import { animateScroll as scroll } from 'react-scroll'
|
||||||
import { LinkedTypebot, useTypebot } from 'contexts/TypebotContext'
|
import { LinkedTypebot, useTypebot } from '@/providers/TypebotProvider'
|
||||||
import { ChatContext } from 'contexts/ChatContext'
|
import { setCssVariablesValue } from '@/features/theme'
|
||||||
|
import { ChatProvider } from '@/providers/ChatProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
@ -139,7 +138,7 @@ export const ConversationContainer = ({
|
|||||||
ref={scrollableContainer}
|
ref={scrollableContainer}
|
||||||
className="overflow-y-scroll w-full lg:w-3/4 min-h-full rounded lg:px-5 px-3 pt-10 relative scrollable-container typebot-chat-view"
|
className="overflow-y-scroll w-full lg:w-3/4 min-h-full rounded lg:px-5 px-3 pt-10 relative scrollable-container typebot-chat-view"
|
||||||
>
|
>
|
||||||
<ChatContext onScroll={autoScrollToBottom}>
|
<ChatProvider onScroll={autoScrollToBottom}>
|
||||||
{displayedGroups.map((displayedGroup, idx) => {
|
{displayedGroups.map((displayedGroup, idx) => {
|
||||||
const groupAfter = displayedGroups[idx + 1]
|
const groupAfter = displayedGroups[idx + 1]
|
||||||
const groupAfterStartsWithInput =
|
const groupAfterStartsWithInput =
|
||||||
@ -158,7 +157,7 @@ export const ConversationContainer = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ChatContext>
|
</ChatProvider>
|
||||||
|
|
||||||
{/* We use a block to simulate padding because it makes iOS scroll flicker */}
|
{/* We use a block to simulate padding because it makes iOS scroll flicker */}
|
||||||
<div className="w-full h-32" ref={bottomAnchor} />
|
<div className="w-full h-32" ref={bottomAnchor} />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { SVGProps } from 'react'
|
import React, { SVGProps } from 'react'
|
||||||
import { SendIcon } from '../../../../assets/icons'
|
import { SendIcon } from './icons'
|
||||||
|
|
||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
label: string
|
label: string
|
@ -1,11 +1,11 @@
|
|||||||
import React, { CSSProperties, useMemo } from 'react'
|
import React, { CSSProperties, useMemo } from 'react'
|
||||||
import { TypebotContext } from '../contexts/TypebotContext'
|
import { TypebotProvider } from '../providers/TypebotProvider'
|
||||||
import Frame from 'react-frame-component'
|
import Frame from 'react-frame-component'
|
||||||
import styles from '../assets/style.css'
|
import styles from '../assets/style.css'
|
||||||
import importantStyles from '../assets/importantStyles.css'
|
import importantStyles from '../assets/importantStyles.css'
|
||||||
import phoneSyle from '../assets/phone.css'
|
import phoneSyle from '../assets/phone.css'
|
||||||
import { ConversationContainer } from './ConversationContainer'
|
import { ConversationContainer } from './ConversationContainer'
|
||||||
import { AnswersContext } from '../contexts/AnswersContext'
|
import { AnswersProvider } from '../providers/AnswersProvider'
|
||||||
import {
|
import {
|
||||||
Answer,
|
Answer,
|
||||||
BackgroundType,
|
BackgroundType,
|
||||||
@ -89,14 +89,14 @@ export const TypebotViewer = ({
|
|||||||
}:wght@300;400;600&display=swap');`,
|
}:wght@300;400;600&display=swap');`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TypebotContext
|
<TypebotProvider
|
||||||
typebot={typebot}
|
typebot={typebot}
|
||||||
apiHost={apiHost}
|
apiHost={apiHost}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
onNewLog={handleNewLog}
|
onNewLog={handleNewLog}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
>
|
>
|
||||||
<AnswersContext
|
<AnswersProvider
|
||||||
resultId={resultId}
|
resultId={resultId}
|
||||||
onNewAnswer={handleNewAnswer}
|
onNewAnswer={handleNewAnswer}
|
||||||
onVariablesUpdated={onVariablesUpdated}
|
onVariablesUpdated={onVariablesUpdated}
|
||||||
@ -120,8 +120,8 @@ export const TypebotViewer = ({
|
|||||||
</div>
|
</div>
|
||||||
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
|
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
|
||||||
</div>
|
</div>
|
||||||
</AnswersContext>
|
</AnswersProvider>
|
||||||
</TypebotContext>
|
</TypebotProvider>
|
||||||
</Frame>
|
</Frame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export const TypingContent = (): JSX.Element => (
|
export const TypingBubble = (): JSX.Element => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-2 h-2 mr-1 rounded-full bubble1" />
|
<div className="w-2 h-2 mr-1 rounded-full bubble1" />
|
||||||
<div className="w-2 h-2 mr-1 rounded-full bubble2" />
|
<div className="w-2 h-2 mr-1 rounded-full bubble2" />
|
25
packages/bot-engine/src/components/inputs/ShortTextInput.tsx
Normal file
25
packages/bot-engine/src/components/inputs/ShortTextInput.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { isMobile } from '@/utils/helpers'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type ShortTextInputProps = {
|
||||||
|
onChange: (value: string) => void
|
||||||
|
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>
|
||||||
|
|
||||||
|
export const ShortTextInput = React.forwardRef(
|
||||||
|
(
|
||||||
|
{ onChange, ...props }: ShortTextInputProps,
|
||||||
|
ref: React.ForwardedRef<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||||
|
type="text"
|
||||||
|
style={{ fontSize: '16px' }}
|
||||||
|
autoFocus={!isMobile}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
25
packages/bot-engine/src/components/inputs/Textarea.tsx
Normal file
25
packages/bot-engine/src/components/inputs/Textarea.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { isMobile } from '@/utils/helpers'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type TextareaProps = {
|
||||||
|
onChange: (value: string) => void
|
||||||
|
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'>
|
||||||
|
|
||||||
|
export const Textarea = React.forwardRef(
|
||||||
|
(
|
||||||
|
{ onChange, ...props }: TextareaProps,
|
||||||
|
ref: React.ForwardedRef<HTMLTextAreaElement>
|
||||||
|
) => (
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||||
|
rows={6}
|
||||||
|
data-testid="textarea"
|
||||||
|
required
|
||||||
|
style={{ fontSize: '16px' }}
|
||||||
|
autoFocus={!isMobile}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { EmbedBubbleBlock } from 'models'
|
import { EmbedBubbleBlock } from 'models'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingBubble } from '../../../../../components/TypingBubble'
|
||||||
import { parseVariables } from 'services/variable'
|
import { parseVariables } from '@/features/variables'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: EmbedBubbleBlock
|
block: EmbedBubbleBlock
|
||||||
@ -56,7 +56,7 @@ export const EmbedBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
height: isTyping ? '2rem' : '100%',
|
height: isTyping ? '2rem' : '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTyping ? <TypingContent /> : <></>}
|
{isTyping ? <TypingBubble /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
id="embed-bubble-content"
|
id="embed-bubble-content"
|
@ -0,0 +1 @@
|
|||||||
|
export { EmbedBubble } from './components/EmbedBubble'
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
import { ImageBubbleBlock } from 'models'
|
import { ImageBubbleBlock } from 'models'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingBubble } from '@/components/TypingBubble'
|
||||||
import { parseVariables } from 'services/variable'
|
import { parseVariables } from '@/features/variables'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: ImageBubbleBlock
|
block: ImageBubbleBlock
|
||||||
@ -61,7 +61,7 @@ export const ImageBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
height: isTyping ? '2rem' : '100%',
|
height: isTyping ? '2rem' : '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTyping ? <TypingContent /> : <></>}
|
{isTyping ? <TypingBubble /> : null}
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
ref={image}
|
ref={image}
|
@ -0,0 +1 @@
|
|||||||
|
export { ImageBubble } from './components/ImageBubble'
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
import { BubbleBlockType, TextBubbleBlock } from 'models'
|
import { BubbleBlockType, TextBubbleBlock } from 'models'
|
||||||
import { computeTypingTimeout } from 'services/chat'
|
import { computeTypingDuration } from '../utils/computeTypingDuration'
|
||||||
import { TypingContent } from './TypingContent'
|
import { parseVariables } from '@/features/variables'
|
||||||
import { parseVariables } from 'services/variable'
|
import { TypingBubble } from '@/components/TypingBubble'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: TextBubbleBlock
|
block: TextBubbleBlock
|
||||||
@ -29,7 +29,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isTyping || isLoading) return
|
if (!isTyping || isLoading) return
|
||||||
const typingTimeout = computeTypingTimeout(
|
const typingTimeout = computeTypingDuration(
|
||||||
block.content.plainText,
|
block.content.plainText,
|
||||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||||
)
|
)
|
||||||
@ -58,7 +58,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
}}
|
}}
|
||||||
data-testid="host-bubble"
|
data-testid="host-bubble"
|
||||||
>
|
>
|
||||||
{isTyping ? <TypingContent /> : <></>}
|
{isTyping ? <TypingBubble /> : null}
|
||||||
</div>
|
</div>
|
||||||
{block.type === BubbleBlockType.TEXT && (
|
{block.type === BubbleBlockType.TEXT && (
|
||||||
<p
|
<p
|
@ -0,0 +1 @@
|
|||||||
|
export { TextBubble } from './components/TextBubble'
|
@ -0,0 +1,16 @@
|
|||||||
|
import { TypingEmulation } from 'models'
|
||||||
|
|
||||||
|
export const computeTypingDuration = (
|
||||||
|
bubbleContent: string,
|
||||||
|
typingSettings: TypingEmulation
|
||||||
|
) => {
|
||||||
|
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
||||||
|
if (wordCount === 0) wordCount = bubbleContent.length
|
||||||
|
const typedWordsPerMinute = typingSettings.speed
|
||||||
|
let typingTimeout = typingSettings.enabled
|
||||||
|
? (wordCount / typedWordsPerMinute) * 60000
|
||||||
|
: 0
|
||||||
|
if (typingTimeout > typingSettings.maxDelay * 1000)
|
||||||
|
typingTimeout = typingSettings.maxDelay * 1000
|
||||||
|
return typingTimeout
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
import {
|
import {
|
||||||
Variable,
|
Variable,
|
||||||
VideoBubbleContent,
|
VideoBubbleContent,
|
||||||
VideoBubbleContentType,
|
VideoBubbleContentType,
|
||||||
VideoBubbleBlock,
|
VideoBubbleBlock,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingBubble } from '@/components/TypingBubble'
|
||||||
import { parseVariables } from 'services/variable'
|
import { parseVariables } from '@/features/variables'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: VideoBubbleBlock
|
block: VideoBubbleBlock
|
||||||
@ -54,7 +54,7 @@ export const VideoBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
height: isTyping ? '2rem' : '100%',
|
height: isTyping ? '2rem' : '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTyping ? <TypingContent /> : <></>}
|
{isTyping ? <TypingBubble /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
<VideoContent
|
<VideoContent
|
||||||
content={block.content}
|
content={block.content}
|
@ -0,0 +1 @@
|
|||||||
|
export { VideoBubble } from './components/VideoBubble'
|
@ -1,10 +1,10 @@
|
|||||||
import { useAnswers } from 'contexts/AnswersContext'
|
import { parseVariables } from '@/features/variables'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useAnswers } from '@/providers/AnswersProvider'
|
||||||
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
import { ChoiceInputBlock } from 'models'
|
import { ChoiceInputBlock } from 'models'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { parseVariables } from 'services/variable'
|
import { SendButton } from '../../../../../components/SendButton'
|
||||||
import { InputSubmitContent } from '../InputChatBlock'
|
|
||||||
import { SendButton } from './SendButton'
|
|
||||||
|
|
||||||
type ChoiceFormProps = {
|
type ChoiceFormProps = {
|
||||||
block: ChoiceInputBlock
|
block: ChoiceInputBlock
|
@ -0,0 +1 @@
|
|||||||
|
export { ChoiceForm } from './components/ChoiceForm'
|
@ -1,8 +1,8 @@
|
|||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
import { DateInputOptions } from 'models'
|
import { DateInputOptions } from 'models'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { parseReadableDate } from 'services/inputs'
|
import { parseReadableDate } from '../utils/parseReadableDate'
|
||||||
import { InputSubmitContent } from '../InputChatBlock'
|
|
||||||
import { SendButton } from './SendButton'
|
|
||||||
|
|
||||||
type DateInputProps = {
|
type DateInputProps = {
|
||||||
onSubmit: (inputValue: InputSubmitContent) => void
|
onSubmit: (inputValue: InputSubmitContent) => void
|
@ -0,0 +1,2 @@
|
|||||||
|
export { DateForm } from './components/DateForm'
|
||||||
|
export { parseReadableDate } from './utils/parseReadableDate'
|
@ -0,0 +1,26 @@
|
|||||||
|
export const parseReadableDate = ({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
hasTime,
|
||||||
|
isRange,
|
||||||
|
}: {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
hasTime?: boolean
|
||||||
|
isRange?: boolean
|
||||||
|
}) => {
|
||||||
|
const currentLocale = window.navigator.language
|
||||||
|
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: hasTime ? '2-digit' : undefined,
|
||||||
|
minute: hasTime ? '2-digit' : undefined,
|
||||||
|
}
|
||||||
|
const fromReadable = new Date(from).toLocaleString(
|
||||||
|
currentLocale,
|
||||||
|
formatOptions
|
||||||
|
)
|
||||||
|
const toReadable = new Date(to).toLocaleString(currentLocale, formatOptions)
|
||||||
|
return `${fromReadable}${isRange ? ` to ${toReadable}` : ''}`
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { ShortTextInput } from '@/components/inputs/ShortTextInput'
|
||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { EmailInputBlock } from 'models'
|
||||||
|
import React, { MutableRefObject, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type EmailInputProps = {
|
||||||
|
block: EmailInputBlock
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
defaultValue?: string
|
||||||
|
hasGuestAvatar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailInput = ({
|
||||||
|
block,
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
hasGuestAvatar,
|
||||||
|
}: EmailInputProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||||
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
const handleChange = (inputValue: string) => setInputValue(inputValue)
|
||||||
|
|
||||||
|
const checkIfInputIsValid = () =>
|
||||||
|
inputValue !== '' && inputRef.current?.reportValidity()
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (checkIfInputIsValid()) onSubmit({ value: inputValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
||||||
|
}
|
||||||
|
data-testid="input"
|
||||||
|
style={{
|
||||||
|
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
|
maxWidth: '350px',
|
||||||
|
}}
|
||||||
|
onKeyDown={submitWhenEnter}
|
||||||
|
>
|
||||||
|
<ShortTextInput
|
||||||
|
ref={inputRef as MutableRefObject<HTMLInputElement>}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={block.options?.labels?.placeholder ?? 'Type your email...'}
|
||||||
|
onChange={handleChange}
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
<SendButton
|
||||||
|
type="button"
|
||||||
|
label={block.options?.labels?.button ?? 'Send'}
|
||||||
|
isDisabled={inputValue === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
|
onClick={submit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export { EmailInput } from './components/EmailInput'
|
||||||
|
export { validateEmail } from './utils/validateEmail'
|
@ -0,0 +1,4 @@
|
|||||||
|
const emailRegex =
|
||||||
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
|
||||||
|
export const validateEmail = (email: string) => emailRegex.test(email)
|
@ -1,10 +1,10 @@
|
|||||||
import { useAnswers } from 'contexts/AnswersContext'
|
import { Spinner, SendButton } from '@/components/SendButton'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useAnswers } from '@/providers/AnswersProvider'
|
||||||
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
import { FileInputBlock } from 'models'
|
import { FileInputBlock } from 'models'
|
||||||
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
||||||
import { uploadFiles } from 'utils'
|
import { uploadFiles } from 'utils'
|
||||||
import { InputSubmitContent } from '../InputChatBlock'
|
|
||||||
import { SendButton, Spinner } from './SendButton'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: FileInputBlock
|
block: FileInputBlock
|
@ -0,0 +1 @@
|
|||||||
|
export { FileUploadForm } from './components/FileUploadForm'
|
@ -0,0 +1,70 @@
|
|||||||
|
import { ShortTextInput } from '@/components/inputs/ShortTextInput'
|
||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { NumberInputBlock } from 'models'
|
||||||
|
import React, { MutableRefObject, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type NumberInputProps = {
|
||||||
|
block: NumberInputBlock
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
defaultValue?: string
|
||||||
|
hasGuestAvatar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NumberInput = ({
|
||||||
|
block,
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
hasGuestAvatar,
|
||||||
|
}: NumberInputProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||||
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
const handleChange = (inputValue: string) => setInputValue(inputValue)
|
||||||
|
|
||||||
|
const checkIfInputIsValid = () =>
|
||||||
|
inputValue !== '' && inputRef.current?.reportValidity()
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (checkIfInputIsValid()) onSubmit({ value: inputValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
||||||
|
}
|
||||||
|
data-testid="input"
|
||||||
|
style={{
|
||||||
|
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
|
maxWidth: '350px',
|
||||||
|
}}
|
||||||
|
onKeyDown={submitWhenEnter}
|
||||||
|
>
|
||||||
|
<ShortTextInput
|
||||||
|
ref={inputRef as MutableRefObject<HTMLInputElement>}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={
|
||||||
|
block.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
|
}
|
||||||
|
onChange={handleChange}
|
||||||
|
type="number"
|
||||||
|
style={{ appearance: 'auto' }}
|
||||||
|
min={block.options?.min}
|
||||||
|
max={block.options?.max}
|
||||||
|
step={block.options?.step ?? 'any'}
|
||||||
|
/>
|
||||||
|
<SendButton
|
||||||
|
type="button"
|
||||||
|
label={block.options?.labels?.button ?? 'Send'}
|
||||||
|
isDisabled={inputValue === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
|
onClick={submit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { NumberInput } from './components/NumberInput'
|
@ -1,14 +1,14 @@
|
|||||||
import React, { FormEvent, useEffect, useState } from 'react'
|
import React, { FormEvent, useEffect, useState } from 'react'
|
||||||
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
|
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
|
||||||
import { Elements } from '@stripe/react-stripe-js'
|
import { Elements } from '@stripe/react-stripe-js'
|
||||||
import { createPaymentIntent } from 'services/stripe'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
|
||||||
import { PaymentInputOptions, Variable } from 'models'
|
import { PaymentInputOptions, Variable } from 'models'
|
||||||
import { SendButton, Spinner } from '../SendButton'
|
import { SendButton, Spinner } from '@/components/SendButton'
|
||||||
import { useFrame } from 'react-frame-component'
|
import { useFrame } from 'react-frame-component'
|
||||||
import { initStripe } from '../../../../../../lib/stripe'
|
import { initStripe } from '@/lib/stripe'
|
||||||
import { parseVariables } from 'services/variable'
|
import { parseVariables } from '@/features/variables'
|
||||||
import { useChat } from 'contexts/ChatContext'
|
import { useChat } from '@/providers/ChatProvider'
|
||||||
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: PaymentInputOptions
|
options: PaymentInputOptions
|
||||||
@ -30,7 +30,7 @@ export const StripePaymentForm = ({ options, onSuccess }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { data, error } = await createPaymentIntent({
|
const { data, error } = await createPaymentIntentQuery({
|
||||||
apiHost,
|
apiHost,
|
||||||
isPreview,
|
isPreview,
|
||||||
variables,
|
variables,
|
@ -0,0 +1 @@
|
|||||||
|
export { PaymentForm } from './components/PaymentForm/'
|
@ -1,7 +1,7 @@
|
|||||||
import { PaymentInputOptions, Variable } from 'models'
|
import { PaymentInputOptions, Variable } from 'models'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
export const createPaymentIntent = ({
|
export const createPaymentIntentQuery = ({
|
||||||
apiHost,
|
apiHost,
|
||||||
isPreview,
|
isPreview,
|
||||||
inputOptions,
|
inputOptions,
|
@ -0,0 +1,68 @@
|
|||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/helpers'
|
||||||
|
import { PhoneNumberInputBlock } from 'models'
|
||||||
|
import React, { useRef, useState } from 'react'
|
||||||
|
import ReactPhoneNumberInput, { Value, Country } from 'react-phone-number-input'
|
||||||
|
|
||||||
|
type PhoneInputProps = {
|
||||||
|
block: PhoneNumberInputBlock
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
defaultValue?: string
|
||||||
|
hasGuestAvatar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhoneInput = ({
|
||||||
|
block,
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
hasGuestAvatar,
|
||||||
|
}: PhoneInputProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inputRef = useRef<any>(null)
|
||||||
|
|
||||||
|
const handleChange = (inputValue: Value | undefined) =>
|
||||||
|
setInputValue(inputValue as string)
|
||||||
|
|
||||||
|
const checkIfInputIsValid = () =>
|
||||||
|
inputValue !== '' && inputRef.current?.reportValidity()
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (checkIfInputIsValid()) onSubmit({ value: inputValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
||||||
|
}
|
||||||
|
data-testid="input"
|
||||||
|
style={{
|
||||||
|
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
|
maxWidth: '350px',
|
||||||
|
}}
|
||||||
|
onKeyDown={submitWhenEnter}
|
||||||
|
>
|
||||||
|
<ReactPhoneNumberInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={block.options.labels.placeholder ?? 'Your phone number...'}
|
||||||
|
defaultCountry={block.options.defaultCountryCode as Country}
|
||||||
|
autoFocus={!isMobile}
|
||||||
|
/>
|
||||||
|
<SendButton
|
||||||
|
type="button"
|
||||||
|
label={block.options?.labels?.button ?? 'Send'}
|
||||||
|
isDisabled={inputValue === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
|
onClick={submit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export { PhoneInput } from './components/PhoneInput'
|
||||||
|
export { validatePhoneNumber } from './utils/validatePhoneNumber'
|
@ -0,0 +1,4 @@
|
|||||||
|
import { isPossiblePhoneNumber } from 'react-phone-number-input'
|
||||||
|
|
||||||
|
export const validatePhoneNumber = (phoneNumber: string) =>
|
||||||
|
isPossiblePhoneNumber(phoneNumber)
|
@ -1,8 +1,8 @@
|
|||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
import { RatingInputOptions, RatingInputBlock } from 'models'
|
import { RatingInputOptions, RatingInputBlock } from 'models'
|
||||||
import React, { FormEvent, useState } from 'react'
|
import React, { FormEvent, useState } from 'react'
|
||||||
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
||||||
import { InputSubmitContent } from '../InputChatBlock'
|
import { SendButton } from '../../../../../components/SendButton'
|
||||||
import { SendButton } from './SendButton'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: RatingInputBlock
|
block: RatingInputBlock
|
@ -0,0 +1 @@
|
|||||||
|
export { RatingForm } from './components/RatingForm'
|
@ -0,0 +1,80 @@
|
|||||||
|
import { ShortTextInput } from '@/components/inputs/ShortTextInput'
|
||||||
|
import { Textarea } from '@/components/inputs/Textarea'
|
||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { TextInputBlock } from 'models'
|
||||||
|
import React, { MutableRefObject, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type TextInputProps = {
|
||||||
|
block: TextInputBlock
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
defaultValue: string | undefined
|
||||||
|
hasGuestAvatar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextInput = ({
|
||||||
|
block,
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
hasGuestAvatar,
|
||||||
|
}: TextInputProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||||
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
const isLongText = block.options?.isLong
|
||||||
|
|
||||||
|
const handleChange = (inputValue: string) => setInputValue(inputValue)
|
||||||
|
|
||||||
|
const checkIfInputIsValid = () =>
|
||||||
|
inputValue !== '' && inputRef.current?.reportValidity()
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (checkIfInputIsValid()) onSubmit({ value: inputValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||||
|
if (isLongText) return
|
||||||
|
if (e.key === 'Enter') submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
||||||
|
}
|
||||||
|
data-testid="input"
|
||||||
|
style={{
|
||||||
|
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
|
maxWidth: isLongText ? undefined : '350px',
|
||||||
|
}}
|
||||||
|
onKeyDown={submitWhenEnter}
|
||||||
|
>
|
||||||
|
{isLongText ? (
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef as MutableRefObject<HTMLTextAreaElement>}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={
|
||||||
|
block.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ShortTextInput
|
||||||
|
ref={inputRef as MutableRefObject<HTMLInputElement>}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={
|
||||||
|
block.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SendButton
|
||||||
|
type="button"
|
||||||
|
label={block.options?.labels?.button ?? 'Send'}
|
||||||
|
isDisabled={inputValue === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
|
onClick={submit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { TextInput } from './components/TextInput'
|
@ -1,45 +1,30 @@
|
|||||||
import {
|
import { ShortTextInput } from '@/components/inputs/ShortTextInput'
|
||||||
EmailInputBlock,
|
import { SendButton } from '@/components/SendButton'
|
||||||
InputBlockType,
|
import { InputSubmitContent } from '@/types'
|
||||||
NumberInputBlock,
|
import { UrlInputBlock } from 'models'
|
||||||
PhoneNumberInputBlock,
|
import React, { MutableRefObject, useRef, useState } from 'react'
|
||||||
TextInputBlock,
|
|
||||||
UrlInputBlock,
|
|
||||||
} from 'models'
|
|
||||||
import React, { useRef, useState } from 'react'
|
|
||||||
import { InputSubmitContent } from '../../InputChatBlock'
|
|
||||||
import { SendButton } from '../SendButton'
|
|
||||||
import { TextInput } from './TextInput'
|
|
||||||
|
|
||||||
type TextFormProps = {
|
type UrlInputProps = {
|
||||||
block:
|
block: UrlInputBlock
|
||||||
| TextInputBlock
|
|
||||||
| EmailInputBlock
|
|
||||||
| NumberInputBlock
|
|
||||||
| UrlInputBlock
|
|
||||||
| PhoneNumberInputBlock
|
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextForm = ({
|
export const UrlInput = ({
|
||||||
block,
|
block,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
hasGuestAvatar,
|
hasGuestAvatar,
|
||||||
}: TextFormProps) => {
|
}: UrlInputProps) => {
|
||||||
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
const isLongText = block.type === InputBlockType.TEXT && block.options?.isLong
|
|
||||||
|
|
||||||
const handleChange = (inputValue: string) => {
|
const handleChange = (inputValue: string) => {
|
||||||
if (block.type === InputBlockType.URL && !inputValue.startsWith('https://'))
|
if (!inputValue.startsWith('https://'))
|
||||||
return inputValue === 'https:/'
|
return inputValue === 'https:/'
|
||||||
? undefined
|
? undefined
|
||||||
: setInputValue(`https://${inputValue}`)
|
: setInputValue(`https://${inputValue}`)
|
||||||
|
|
||||||
setInputValue(inputValue)
|
setInputValue(inputValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +36,6 @@ export const TextForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||||
if (block.type === InputBlockType.TEXT && block.options.isLong) return
|
|
||||||
if (e.key === 'Enter') submit()
|
if (e.key === 'Enter') submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,15 +47,17 @@ export const TextForm = ({
|
|||||||
data-testid="input"
|
data-testid="input"
|
||||||
style={{
|
style={{
|
||||||
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
maxWidth: isLongText ? undefined : '350px',
|
maxWidth: '350px',
|
||||||
}}
|
}}
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
>
|
>
|
||||||
<TextInput
|
<ShortTextInput
|
||||||
inputRef={inputRef}
|
ref={inputRef as MutableRefObject<HTMLInputElement>}
|
||||||
block={block}
|
|
||||||
onChange={handleChange}
|
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
|
placeholder={block.options?.labels?.placeholder ?? 'Type your URL...'}
|
||||||
|
onChange={handleChange}
|
||||||
|
type="url"
|
||||||
|
autoComplete="url"
|
||||||
/>
|
/>
|
||||||
<SendButton
|
<SendButton
|
||||||
type="button"
|
type="button"
|
@ -0,0 +1,2 @@
|
|||||||
|
export { UrlInput } from './components/UrlInput'
|
||||||
|
export { validateUrl } from './utils/validateUrl'
|
@ -0,0 +1,4 @@
|
|||||||
|
const urlRegex =
|
||||||
|
/^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/
|
||||||
|
|
||||||
|
export const validateUrl = (url: string) => urlRegex.test(url)
|
@ -0,0 +1 @@
|
|||||||
|
export * from './utils/executeChatwootBlock'
|
@ -1,8 +1,8 @@
|
|||||||
|
import { parseVariables, parseCorrectValueType } from '@/features/variables'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import { sendEventToParent } from '@/utils/chat'
|
||||||
|
import { isEmbedded } from '@/utils/helpers'
|
||||||
import { ChatwootBlock, ChatwootOptions } from 'models'
|
import { ChatwootBlock, ChatwootOptions } from 'models'
|
||||||
import { sendEventToParent } from 'services/chat'
|
|
||||||
import { IntegrationContext } from 'services/integration'
|
|
||||||
import { isEmbedded } from 'services/utils'
|
|
||||||
import { parseCorrectValueType, parseVariables } from 'services/variable'
|
|
||||||
|
|
||||||
const parseSetUserCode = (user: ChatwootOptions['user']) => `
|
const parseSetUserCode = (user: ChatwootOptions['user']) => `
|
||||||
window.$chatwoot.setUser("${user?.id ?? ''}", {
|
window.$chatwoot.setUser("${user?.id ?? ''}", {
|
||||||
@ -47,9 +47,9 @@ if (window.$chatwoot) {
|
|||||||
})(document, "script");
|
})(document, "script");
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const openChatwootWidget = async (
|
export const executeChatwootBlock = async (
|
||||||
block: ChatwootBlock,
|
block: ChatwootBlock,
|
||||||
{ variables, isPreview, onNewLog }: IntegrationContext
|
{ variables, isPreview, onNewLog }: IntegrationState
|
||||||
) => {
|
) => {
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
onNewLog({
|
onNewLog({
|
@ -0,0 +1 @@
|
|||||||
|
export { executeGoogleAnalyticsBlock } from './utils/executeGoogleAnalyticsBlock'
|
@ -0,0 +1,15 @@
|
|||||||
|
import { parseVariablesInObject } from '@/features/variables'
|
||||||
|
import { sendGaEvent } from '@/lib/gtag'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import { GoogleAnalyticsBlock } from 'models'
|
||||||
|
|
||||||
|
export const executeGoogleAnalyticsBlock = async (
|
||||||
|
block: GoogleAnalyticsBlock,
|
||||||
|
{ variables }: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (!block.options?.trackingId) return block.outgoingEdgeId
|
||||||
|
const { default: initGoogleAnalytics } = await import('@/lib/gtag')
|
||||||
|
await initGoogleAnalytics(block.options.trackingId)
|
||||||
|
sendGaEvent(parseVariablesInObject(block.options, variables))
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeGoogleSheetBlock } from './utils/executeGoogleSheetBlock'
|
@ -0,0 +1,159 @@
|
|||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import { parseLog } from '@/utils/helpers'
|
||||||
|
import {
|
||||||
|
GoogleSheetsBlock,
|
||||||
|
GoogleSheetsAction,
|
||||||
|
GoogleSheetsInsertRowOptions,
|
||||||
|
GoogleSheetsUpdateRowOptions,
|
||||||
|
GoogleSheetsGetOptions,
|
||||||
|
VariableWithValue,
|
||||||
|
Cell,
|
||||||
|
Variable,
|
||||||
|
} from 'models'
|
||||||
|
import { stringify } from 'qs'
|
||||||
|
import { sendRequest, byId } from 'utils'
|
||||||
|
|
||||||
|
export const executeGoogleSheetBlock = async (
|
||||||
|
block: GoogleSheetsBlock,
|
||||||
|
context: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (!('action' in block.options)) return block.outgoingEdgeId
|
||||||
|
switch (block.options.action) {
|
||||||
|
case GoogleSheetsAction.INSERT_ROW:
|
||||||
|
await insertRowInGoogleSheets(block.options, context)
|
||||||
|
break
|
||||||
|
case GoogleSheetsAction.UPDATE_ROW:
|
||||||
|
await updateRowInGoogleSheets(block.options, context)
|
||||||
|
break
|
||||||
|
case GoogleSheetsAction.GET:
|
||||||
|
await getRowFromGoogleSheets(block.options, context)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertRowInGoogleSheets = async (
|
||||||
|
options: GoogleSheetsInsertRowOptions,
|
||||||
|
{ variables, apiHost, onNewLog, resultId }: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (!options.cellsToInsert) {
|
||||||
|
onNewLog({
|
||||||
|
status: 'warning',
|
||||||
|
description: 'Cells to insert are undefined',
|
||||||
|
details: null,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const params = stringify({ resultId })
|
||||||
|
const { error } = await sendRequest({
|
||||||
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
credentialsId: options.credentialsId,
|
||||||
|
values: parseCellValues(options.cellsToInsert, variables),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully inserted a row in the sheet',
|
||||||
|
'Failed to insert a row in the sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRowInGoogleSheets = async (
|
||||||
|
options: GoogleSheetsUpdateRowOptions,
|
||||||
|
{ variables, apiHost, onNewLog, resultId }: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (!options.cellsToUpsert || !options.referenceCell) return
|
||||||
|
const params = stringify({ resultId })
|
||||||
|
const { error } = await sendRequest({
|
||||||
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
credentialsId: options.credentialsId,
|
||||||
|
values: parseCellValues(options.cellsToUpsert, variables),
|
||||||
|
referenceCell: {
|
||||||
|
column: options.referenceCell.column,
|
||||||
|
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully updated a row in the sheet',
|
||||||
|
'Failed to update a row in the sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowFromGoogleSheets = async (
|
||||||
|
options: GoogleSheetsGetOptions,
|
||||||
|
{
|
||||||
|
variables,
|
||||||
|
updateVariableValue,
|
||||||
|
updateVariables,
|
||||||
|
apiHost,
|
||||||
|
onNewLog,
|
||||||
|
resultId,
|
||||||
|
}: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (!options.referenceCell || !options.cellsToExtract) return
|
||||||
|
const queryParams = stringify(
|
||||||
|
{
|
||||||
|
credentialsId: options.credentialsId,
|
||||||
|
referenceCell: {
|
||||||
|
column: options.referenceCell.column,
|
||||||
|
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||||
|
},
|
||||||
|
columns: options.cellsToExtract.map((cell) => cell.column),
|
||||||
|
resultId,
|
||||||
|
},
|
||||||
|
{ indices: false }
|
||||||
|
)
|
||||||
|
const { data, error } = await sendRequest<{ [key: string]: string }>({
|
||||||
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully fetched data from sheet',
|
||||||
|
'Failed to fetch data from sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!data) return
|
||||||
|
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
||||||
|
(newVariables, cell) => {
|
||||||
|
const existingVariable = variables.find(byId(cell.variableId))
|
||||||
|
const value = data[cell.column ?? ''] ?? null
|
||||||
|
if (!existingVariable) return newVariables
|
||||||
|
updateVariableValue(existingVariable.id, value)
|
||||||
|
return [
|
||||||
|
...newVariables,
|
||||||
|
{
|
||||||
|
...existingVariable,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
updateVariables(newVariables)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCellValues = (
|
||||||
|
cells: Cell[],
|
||||||
|
variables: Variable[]
|
||||||
|
): { [key: string]: string } =>
|
||||||
|
cells.reduce((row, cell) => {
|
||||||
|
return !cell.column || !cell.value
|
||||||
|
? row
|
||||||
|
: {
|
||||||
|
...row,
|
||||||
|
[cell.column]: parseVariables(variables)(cell.value),
|
||||||
|
}
|
||||||
|
}, {})
|
@ -0,0 +1 @@
|
|||||||
|
export { executeSendEmailBlock } from './utils/executeSendEmailBlock'
|
@ -0,0 +1,51 @@
|
|||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import { parseLog } from '@/utils/helpers'
|
||||||
|
import { SendEmailBlock } from 'models'
|
||||||
|
import { sendRequest, byId } from 'utils'
|
||||||
|
|
||||||
|
export const executeSendEmailBlock = async (
|
||||||
|
block: SendEmailBlock,
|
||||||
|
{
|
||||||
|
variables,
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
onNewLog,
|
||||||
|
resultId,
|
||||||
|
typebotId,
|
||||||
|
resultValues,
|
||||||
|
}: IntegrationState
|
||||||
|
) => {
|
||||||
|
if (isPreview) {
|
||||||
|
onNewLog({
|
||||||
|
status: 'info',
|
||||||
|
description: 'Emails are not sent in preview mode',
|
||||||
|
details: null,
|
||||||
|
})
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
||||||
|
const { options } = block
|
||||||
|
const { error } = await sendRequest({
|
||||||
|
url: `${apiHost}/api/typebots/${typebotId}/integrations/email?resultId=${resultId}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
credentialsId: options.credentialsId,
|
||||||
|
recipients: options.recipients.map(parseVariables(variables)),
|
||||||
|
subject: parseVariables(variables)(options.subject ?? ''),
|
||||||
|
body: parseVariables(variables)(options.body ?? ''),
|
||||||
|
cc: (options.cc ?? []).map(parseVariables(variables)),
|
||||||
|
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
||||||
|
replyTo: options.replyTo
|
||||||
|
? parseVariables(variables)(options.replyTo)
|
||||||
|
: undefined,
|
||||||
|
fileUrls: variables.find(byId(options.attachmentsVariableId))?.value,
|
||||||
|
isCustomBody: options.isCustomBody,
|
||||||
|
isBodyCode: options.isBodyCode,
|
||||||
|
resultValues,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(error, 'Succesfully sent an email', 'Failed to send an email')
|
||||||
|
)
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeWebhook } from './utils/executeWebhookBlock'
|
@ -0,0 +1,69 @@
|
|||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import {
|
||||||
|
WebhookBlock,
|
||||||
|
ZapierBlock,
|
||||||
|
MakeComBlock,
|
||||||
|
PabblyConnectBlock,
|
||||||
|
VariableWithUnknowValue,
|
||||||
|
} from 'models'
|
||||||
|
import { stringify } from 'qs'
|
||||||
|
import { sendRequest, byId } from 'utils'
|
||||||
|
|
||||||
|
export const executeWebhook = async (
|
||||||
|
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock,
|
||||||
|
{
|
||||||
|
blockId,
|
||||||
|
variables,
|
||||||
|
updateVariableValue,
|
||||||
|
updateVariables,
|
||||||
|
typebotId,
|
||||||
|
apiHost,
|
||||||
|
resultValues,
|
||||||
|
onNewLog,
|
||||||
|
resultId,
|
||||||
|
}: IntegrationState
|
||||||
|
) => {
|
||||||
|
const params = stringify({ resultId })
|
||||||
|
const { data, error } = await sendRequest({
|
||||||
|
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook?${params}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
variables,
|
||||||
|
resultValues,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const statusCode = (
|
||||||
|
data as Record<string, string> | undefined
|
||||||
|
)?.statusCode.toString()
|
||||||
|
const isError = statusCode
|
||||||
|
? statusCode?.startsWith('4') || statusCode?.startsWith('5')
|
||||||
|
: true
|
||||||
|
onNewLog({
|
||||||
|
status: error ? 'error' : isError ? 'warning' : 'success',
|
||||||
|
description: isError
|
||||||
|
? 'Webhook returned an error'
|
||||||
|
: 'Webhook successfuly executed',
|
||||||
|
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
|
||||||
|
})
|
||||||
|
const newVariables = block.options.responseVariableMapping.reduce<
|
||||||
|
VariableWithUnknowValue[]
|
||||||
|
>((newVariables, varMapping) => {
|
||||||
|
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||||
|
const existingVariable = variables.find(byId(varMapping.variableId))
|
||||||
|
if (!existingVariable) return newVariables
|
||||||
|
const func = Function(
|
||||||
|
'data',
|
||||||
|
`return data.${parseVariables(variables)(varMapping?.bodyPath)}`
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const value: unknown = func(data)
|
||||||
|
updateVariableValue(existingVariable?.id, value)
|
||||||
|
return [...newVariables, { ...existingVariable, value }]
|
||||||
|
} catch (err) {
|
||||||
|
return newVariables
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
updateVariables(newVariables)
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeCode } from './utils/executeCode'
|
@ -0,0 +1,29 @@
|
|||||||
|
import { parseVariables, parseCorrectValueType } from '@/features/variables'
|
||||||
|
import { LogicState } from '@/types'
|
||||||
|
import { sendEventToParent } from '@/utils/chat'
|
||||||
|
import { isEmbedded } from '@/utils/helpers'
|
||||||
|
import { CodeBlock } from 'models'
|
||||||
|
|
||||||
|
export const executeCode = async (
|
||||||
|
block: CodeBlock,
|
||||||
|
{ typebot: { variables } }: LogicState
|
||||||
|
) => {
|
||||||
|
if (!block.options.content) return
|
||||||
|
if (block.options.shouldExecuteInParentContext && isEmbedded) {
|
||||||
|
sendEventToParent({
|
||||||
|
codeToExecute: parseVariables(variables)(block.options.content),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const func = Function(
|
||||||
|
...variables.map((v) => v.id),
|
||||||
|
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await func(...variables.map((v) => parseCorrectValueType(v.value)))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeCondition } from './utils/executeCondition'
|
@ -0,0 +1,54 @@
|
|||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { EdgeId, LogicState } from '@/types'
|
||||||
|
import {
|
||||||
|
Comparison,
|
||||||
|
ComparisonOperators,
|
||||||
|
ConditionBlock,
|
||||||
|
LogicalOperator,
|
||||||
|
Variable,
|
||||||
|
} from 'models'
|
||||||
|
import { isNotDefined, isDefined } from 'utils'
|
||||||
|
|
||||||
|
export const executeCondition = (
|
||||||
|
block: ConditionBlock,
|
||||||
|
{ typebot: { variables } }: LogicState
|
||||||
|
): EdgeId | undefined => {
|
||||||
|
const { content } = block.items[0]
|
||||||
|
const isConditionPassed =
|
||||||
|
content.logicalOperator === LogicalOperator.AND
|
||||||
|
? content.comparisons.every(executeComparison(variables))
|
||||||
|
: content.comparisons.some(executeComparison(variables))
|
||||||
|
return isConditionPassed
|
||||||
|
? block.items[0].outgoingEdgeId
|
||||||
|
: block.outgoingEdgeId
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeComparison =
|
||||||
|
(variables: Variable[]) => (comparison: Comparison) => {
|
||||||
|
if (!comparison?.variableId) return false
|
||||||
|
const inputValue = (
|
||||||
|
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
||||||
|
).trim()
|
||||||
|
const value = parseVariables(variables)(comparison.value).trim()
|
||||||
|
if (isNotDefined(value)) return false
|
||||||
|
switch (comparison.comparisonOperator) {
|
||||||
|
case ComparisonOperators.CONTAINS: {
|
||||||
|
return inputValue.toLowerCase().includes(value.toLowerCase())
|
||||||
|
}
|
||||||
|
case ComparisonOperators.EQUAL: {
|
||||||
|
return inputValue === value
|
||||||
|
}
|
||||||
|
case ComparisonOperators.NOT_EQUAL: {
|
||||||
|
return inputValue !== value
|
||||||
|
}
|
||||||
|
case ComparisonOperators.GREATER: {
|
||||||
|
return parseFloat(inputValue) > parseFloat(value)
|
||||||
|
}
|
||||||
|
case ComparisonOperators.LESS: {
|
||||||
|
return parseFloat(inputValue) < parseFloat(value)
|
||||||
|
}
|
||||||
|
case ComparisonOperators.IS_SET: {
|
||||||
|
return isDefined(inputValue) && inputValue.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeRedirect } from './utils/executeRedirect'
|
@ -0,0 +1,27 @@
|
|||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
import { EdgeId, LogicState } from '@/types'
|
||||||
|
import { sendEventToParent } from '@/utils/chat'
|
||||||
|
import { RedirectBlock } from 'models'
|
||||||
|
import { sanitizeUrl } from 'utils'
|
||||||
|
|
||||||
|
export const executeRedirect = (
|
||||||
|
block: RedirectBlock,
|
||||||
|
{ typebot: { variables } }: LogicState
|
||||||
|
): EdgeId | undefined => {
|
||||||
|
if (!block.options?.url) return block.outgoingEdgeId
|
||||||
|
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
||||||
|
const isEmbedded = window.parent && window.location !== window.top?.location
|
||||||
|
if (isEmbedded) {
|
||||||
|
if (!block.options.isNewTab)
|
||||||
|
return ((window.top as Window).location.href = formattedUrl)
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.open(formattedUrl)
|
||||||
|
} catch (err) {
|
||||||
|
sendEventToParent({ redirectUrl: formattedUrl })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self')
|
||||||
|
}
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeSetVariable } from './utils/executeSetVariable'
|
@ -0,0 +1,36 @@
|
|||||||
|
import { SetVariableBlock, Variable } from 'models'
|
||||||
|
import { byId } from 'utils'
|
||||||
|
import { EdgeId, LogicState } from '@/types'
|
||||||
|
import { parseVariables, parseCorrectValueType } from '@/features/variables'
|
||||||
|
|
||||||
|
export const executeSetVariable = (
|
||||||
|
block: SetVariableBlock,
|
||||||
|
{ typebot: { variables }, updateVariableValue, updateVariables }: LogicState
|
||||||
|
): EdgeId | undefined => {
|
||||||
|
if (!block.options?.variableId) return block.outgoingEdgeId
|
||||||
|
const evaluatedExpression = block.options.expressionToEvaluate
|
||||||
|
? evaluateSetVariableExpression(variables)(
|
||||||
|
block.options.expressionToEvaluate
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
const existingVariable = variables.find(byId(block.options.variableId))
|
||||||
|
if (!existingVariable) return block.outgoingEdgeId
|
||||||
|
updateVariableValue(existingVariable.id, evaluatedExpression)
|
||||||
|
updateVariables([{ ...existingVariable, value: evaluatedExpression }])
|
||||||
|
return block.outgoingEdgeId
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluateSetVariableExpression =
|
||||||
|
(variables: Variable[]) =>
|
||||||
|
(str: string): unknown => {
|
||||||
|
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
||||||
|
str.includes('return ') ? str : `return ${str}`
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const func = Function(...variables.map((v) => v.id), evaluating)
|
||||||
|
return func(...variables.map((v) => parseCorrectValueType(v.value)))
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Evaluating: ${evaluating}`, err)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { executeTypebotLink } from './utils/executeTypebotLink'
|
@ -0,0 +1,19 @@
|
|||||||
|
import { LinkedTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { LogicState } from '@/types'
|
||||||
|
import { TypebotLinkBlock, Typebot, PublicTypebot } from 'models'
|
||||||
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
|
export const fetchAndInjectTypebot = async (
|
||||||
|
block: TypebotLinkBlock,
|
||||||
|
{ apiHost, injectLinkedTypebot, isPreview }: LogicState
|
||||||
|
): Promise<LinkedTypebot | undefined> => {
|
||||||
|
const { data, error } = isPreview
|
||||||
|
? await sendRequest<{ typebot: Typebot }>(
|
||||||
|
`/api/typebots/${block.options.typebotId}`
|
||||||
|
)
|
||||||
|
: await sendRequest<{ typebot: PublicTypebot }>(
|
||||||
|
`${apiHost}/api/publicTypebots/${block.options.typebotId}`
|
||||||
|
)
|
||||||
|
if (!data || error) return
|
||||||
|
return injectLinkedTypebot(data.typebot)
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { LinkedTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { EdgeId, LogicState } from '@/types'
|
||||||
|
import { TypebotLinkBlock, Edge, PublicTypebot } from 'models'
|
||||||
|
import { byId } from 'utils'
|
||||||
|
import { fetchAndInjectTypebot } from '../queries/fetchAndInjectTypebotQuery'
|
||||||
|
|
||||||
|
export const executeTypebotLink = async (
|
||||||
|
block: TypebotLinkBlock,
|
||||||
|
context: LogicState
|
||||||
|
): Promise<{
|
||||||
|
nextEdgeId?: EdgeId
|
||||||
|
linkedTypebot?: PublicTypebot | LinkedTypebot
|
||||||
|
}> => {
|
||||||
|
const {
|
||||||
|
typebot,
|
||||||
|
linkedTypebots,
|
||||||
|
onNewLog,
|
||||||
|
createEdge,
|
||||||
|
setCurrentTypebotId,
|
||||||
|
pushEdgeIdInLinkedTypebotQueue,
|
||||||
|
currentTypebotId,
|
||||||
|
} = context
|
||||||
|
const linkedTypebot = (
|
||||||
|
block.options.typebotId === 'current'
|
||||||
|
? typebot
|
||||||
|
: [typebot, ...linkedTypebots].find(byId(block.options.typebotId)) ??
|
||||||
|
(await fetchAndInjectTypebot(block, context))
|
||||||
|
) as PublicTypebot | LinkedTypebot | undefined
|
||||||
|
if (!linkedTypebot) {
|
||||||
|
onNewLog({
|
||||||
|
status: 'error',
|
||||||
|
description: 'Failed to link typebot',
|
||||||
|
details: '',
|
||||||
|
})
|
||||||
|
return { nextEdgeId: block.outgoingEdgeId }
|
||||||
|
}
|
||||||
|
if (block.outgoingEdgeId)
|
||||||
|
pushEdgeIdInLinkedTypebotQueue({
|
||||||
|
edgeId: block.outgoingEdgeId,
|
||||||
|
typebotId: currentTypebotId,
|
||||||
|
})
|
||||||
|
setCurrentTypebotId(
|
||||||
|
'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id
|
||||||
|
)
|
||||||
|
const nextGroupId =
|
||||||
|
block.options.groupId ??
|
||||||
|
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
||||||
|
?.id
|
||||||
|
if (!nextGroupId) return { nextEdgeId: block.outgoingEdgeId }
|
||||||
|
const newEdge: Edge = {
|
||||||
|
id: (Math.random() * 1000).toString(),
|
||||||
|
from: { blockId: '', groupId: '' },
|
||||||
|
to: {
|
||||||
|
groupId: nextGroupId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createEdge(newEdge)
|
||||||
|
return {
|
||||||
|
nextEdgeId: newEdge.id,
|
||||||
|
linkedTypebot: {
|
||||||
|
...linkedTypebot,
|
||||||
|
edges: [...linkedTypebot.edges, newEdge],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export { openChatwootWidget } from './utils/openChatwootWidget'
|
|
1
packages/bot-engine/src/features/theme/index.ts
Normal file
1
packages/bot-engine/src/features/theme/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './utils/setCssVariablesValue'
|
1
packages/bot-engine/src/features/variables/index.ts
Normal file
1
packages/bot-engine/src/features/variables/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './utils'
|
@ -1,3 +1,3 @@
|
|||||||
export * from './components/TypebotViewer'
|
export * from './components/TypebotViewer'
|
||||||
export { parseVariables } from './services/variable'
|
export { parseVariables } from '@/features/variables'
|
||||||
export * from 'util'
|
export * from 'util'
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { safeStringify } from '@/features/variables'
|
||||||
import {
|
import {
|
||||||
Answer,
|
Answer,
|
||||||
ResultValues,
|
ResultValues,
|
||||||
@ -5,7 +6,6 @@ import {
|
|||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||||
import { safeStringify } from 'services/variable'
|
|
||||||
|
|
||||||
const answersContext = createContext<{
|
const answersContext = createContext<{
|
||||||
resultId?: string
|
resultId?: string
|
||||||
@ -18,7 +18,7 @@ const answersContext = createContext<{
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const AnswersContext = ({
|
export const AnswersProvider = ({
|
||||||
children,
|
children,
|
||||||
resultId,
|
resultId,
|
||||||
onNewAnswer,
|
onNewAnswer,
|
@ -6,7 +6,7 @@ const chatContext = createContext<{
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const ChatContext = ({
|
export const ChatProvider = ({
|
||||||
children,
|
children,
|
||||||
onScroll,
|
onScroll,
|
||||||
}: {
|
}: {
|
@ -1,4 +1,6 @@
|
|||||||
import { TypebotViewerProps } from 'components/TypebotViewer'
|
import { TypebotViewerProps } from '@/components/TypebotViewer'
|
||||||
|
import { safeStringify } from '@/features/variables'
|
||||||
|
import { sendEventToParent } from '@/utils/chat'
|
||||||
import { Log } from 'db'
|
import { Log } from 'db'
|
||||||
import { Edge, PublicTypebot, Typebot } from 'models'
|
import { Edge, PublicTypebot, Typebot } from 'models'
|
||||||
import React, {
|
import React, {
|
||||||
@ -8,8 +10,6 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { sendEventToParent } from 'services/chat'
|
|
||||||
import { safeStringify } from 'services/variable'
|
|
||||||
|
|
||||||
export type LinkedTypebot = Pick<
|
export type LinkedTypebot = Pick<
|
||||||
PublicTypebot | Typebot,
|
PublicTypebot | Typebot,
|
||||||
@ -43,7 +43,7 @@ const typebotContext = createContext<{
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const TypebotContext = ({
|
export const TypebotProvider = ({
|
||||||
children,
|
children,
|
||||||
typebot,
|
typebot,
|
||||||
apiHost,
|
apiHost,
|
@ -1,334 +0,0 @@
|
|||||||
import { Log } from 'db'
|
|
||||||
import { openChatwootWidget } from 'features/chatwoot'
|
|
||||||
import {
|
|
||||||
IntegrationBlock,
|
|
||||||
IntegrationBlockType,
|
|
||||||
GoogleSheetsBlock,
|
|
||||||
GoogleSheetsAction,
|
|
||||||
GoogleSheetsInsertRowOptions,
|
|
||||||
Variable,
|
|
||||||
GoogleSheetsUpdateRowOptions,
|
|
||||||
Cell,
|
|
||||||
GoogleSheetsGetOptions,
|
|
||||||
GoogleAnalyticsBlock,
|
|
||||||
WebhookBlock,
|
|
||||||
SendEmailBlock,
|
|
||||||
ZapierBlock,
|
|
||||||
ResultValues,
|
|
||||||
Group,
|
|
||||||
VariableWithValue,
|
|
||||||
MakeComBlock,
|
|
||||||
PabblyConnectBlock,
|
|
||||||
VariableWithUnknowValue,
|
|
||||||
} from 'models'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import { byId, sendRequest } from 'utils'
|
|
||||||
import { sendGaEvent } from '../../lib/gtag'
|
|
||||||
import { parseVariables, parseVariablesInObject } from './variable'
|
|
||||||
|
|
||||||
export type IntegrationContext = {
|
|
||||||
apiHost: string
|
|
||||||
typebotId: string
|
|
||||||
groupId: string
|
|
||||||
blockId: string
|
|
||||||
isPreview: boolean
|
|
||||||
variables: Variable[]
|
|
||||||
resultValues: ResultValues
|
|
||||||
groups: Group[]
|
|
||||||
resultId?: string
|
|
||||||
updateVariables: (variables: VariableWithUnknowValue[]) => void
|
|
||||||
updateVariableValue: (variableId: string, value: unknown) => void
|
|
||||||
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const executeIntegration = ({
|
|
||||||
block,
|
|
||||||
context,
|
|
||||||
}: {
|
|
||||||
block: IntegrationBlock
|
|
||||||
context: IntegrationContext
|
|
||||||
}): Promise<string | undefined> => {
|
|
||||||
switch (block.type) {
|
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
|
||||||
return executeGoogleSheetIntegration(block, context)
|
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
|
||||||
return executeGoogleAnalyticsIntegration(block, context)
|
|
||||||
case IntegrationBlockType.ZAPIER:
|
|
||||||
case IntegrationBlockType.MAKE_COM:
|
|
||||||
case IntegrationBlockType.PABBLY_CONNECT:
|
|
||||||
case IntegrationBlockType.WEBHOOK:
|
|
||||||
return executeWebhook(block, context)
|
|
||||||
case IntegrationBlockType.EMAIL:
|
|
||||||
return sendEmail(block, context)
|
|
||||||
case IntegrationBlockType.CHATWOOT:
|
|
||||||
return openChatwootWidget(block, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const executeGoogleAnalyticsIntegration = async (
|
|
||||||
block: GoogleAnalyticsBlock,
|
|
||||||
{ variables }: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (!block.options?.trackingId) return block.outgoingEdgeId
|
|
||||||
const { default: initGoogleAnalytics } = await import('../../lib/gtag')
|
|
||||||
await initGoogleAnalytics(block.options.trackingId)
|
|
||||||
sendGaEvent(parseVariablesInObject(block.options, variables))
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeGoogleSheetIntegration = async (
|
|
||||||
block: GoogleSheetsBlock,
|
|
||||||
context: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (!('action' in block.options)) return block.outgoingEdgeId
|
|
||||||
switch (block.options.action) {
|
|
||||||
case GoogleSheetsAction.INSERT_ROW:
|
|
||||||
await insertRowInGoogleSheets(block.options, context)
|
|
||||||
break
|
|
||||||
case GoogleSheetsAction.UPDATE_ROW:
|
|
||||||
await updateRowInGoogleSheets(block.options, context)
|
|
||||||
break
|
|
||||||
case GoogleSheetsAction.GET:
|
|
||||||
await getRowFromGoogleSheets(block.options, context)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertRowInGoogleSheets = async (
|
|
||||||
options: GoogleSheetsInsertRowOptions,
|
|
||||||
{ variables, apiHost, onNewLog, resultId }: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (!options.cellsToInsert) {
|
|
||||||
onNewLog({
|
|
||||||
status: 'warning',
|
|
||||||
description: 'Cells to insert are undefined',
|
|
||||||
details: null,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const params = stringify({ resultId })
|
|
||||||
const { error } = await sendRequest({
|
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
credentialsId: options.credentialsId,
|
|
||||||
values: parseCellValues(options.cellsToInsert, variables),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
onNewLog(
|
|
||||||
parseLog(
|
|
||||||
error,
|
|
||||||
'Succesfully inserted a row in the sheet',
|
|
||||||
'Failed to insert a row in the sheet'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRowInGoogleSheets = async (
|
|
||||||
options: GoogleSheetsUpdateRowOptions,
|
|
||||||
{ variables, apiHost, onNewLog, resultId }: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (!options.cellsToUpsert || !options.referenceCell) return
|
|
||||||
const params = stringify({ resultId })
|
|
||||||
const { error } = await sendRequest({
|
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
|
|
||||||
method: 'PATCH',
|
|
||||||
body: {
|
|
||||||
credentialsId: options.credentialsId,
|
|
||||||
values: parseCellValues(options.cellsToUpsert, variables),
|
|
||||||
referenceCell: {
|
|
||||||
column: options.referenceCell.column,
|
|
||||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
onNewLog(
|
|
||||||
parseLog(
|
|
||||||
error,
|
|
||||||
'Succesfully updated a row in the sheet',
|
|
||||||
'Failed to update a row in the sheet'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRowFromGoogleSheets = async (
|
|
||||||
options: GoogleSheetsGetOptions,
|
|
||||||
{
|
|
||||||
variables,
|
|
||||||
updateVariableValue,
|
|
||||||
updateVariables,
|
|
||||||
apiHost,
|
|
||||||
onNewLog,
|
|
||||||
resultId,
|
|
||||||
}: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (!options.referenceCell || !options.cellsToExtract) return
|
|
||||||
const queryParams = stringify(
|
|
||||||
{
|
|
||||||
credentialsId: options.credentialsId,
|
|
||||||
referenceCell: {
|
|
||||||
column: options.referenceCell.column,
|
|
||||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
|
||||||
},
|
|
||||||
columns: options.cellsToExtract.map((cell) => cell.column),
|
|
||||||
resultId,
|
|
||||||
},
|
|
||||||
{ indices: false }
|
|
||||||
)
|
|
||||||
const { data, error } = await sendRequest<{ [key: string]: string }>({
|
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
onNewLog(
|
|
||||||
parseLog(
|
|
||||||
error,
|
|
||||||
'Succesfully fetched data from sheet',
|
|
||||||
'Failed to fetch data from sheet'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (!data) return
|
|
||||||
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
|
||||||
(newVariables, cell) => {
|
|
||||||
const existingVariable = variables.find(byId(cell.variableId))
|
|
||||||
const value = data[cell.column ?? ''] ?? null
|
|
||||||
if (!existingVariable) return newVariables
|
|
||||||
updateVariableValue(existingVariable.id, value)
|
|
||||||
return [
|
|
||||||
...newVariables,
|
|
||||||
{
|
|
||||||
...existingVariable,
|
|
||||||
value,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
updateVariables(newVariables)
|
|
||||||
}
|
|
||||||
const parseCellValues = (
|
|
||||||
cells: Cell[],
|
|
||||||
variables: Variable[]
|
|
||||||
): { [key: string]: string } =>
|
|
||||||
cells.reduce((row, cell) => {
|
|
||||||
return !cell.column || !cell.value
|
|
||||||
? row
|
|
||||||
: {
|
|
||||||
...row,
|
|
||||||
[cell.column]: parseVariables(variables)(cell.value),
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const executeWebhook = async (
|
|
||||||
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock,
|
|
||||||
{
|
|
||||||
blockId,
|
|
||||||
variables,
|
|
||||||
updateVariableValue,
|
|
||||||
updateVariables,
|
|
||||||
typebotId,
|
|
||||||
apiHost,
|
|
||||||
resultValues,
|
|
||||||
onNewLog,
|
|
||||||
resultId,
|
|
||||||
}: IntegrationContext
|
|
||||||
) => {
|
|
||||||
const params = stringify({ resultId })
|
|
||||||
const { data, error } = await sendRequest({
|
|
||||||
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook?${params}`,
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
variables,
|
|
||||||
resultValues,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const statusCode = (
|
|
||||||
data as Record<string, string> | undefined
|
|
||||||
)?.statusCode.toString()
|
|
||||||
const isError = statusCode
|
|
||||||
? statusCode?.startsWith('4') || statusCode?.startsWith('5')
|
|
||||||
: true
|
|
||||||
onNewLog({
|
|
||||||
status: error ? 'error' : isError ? 'warning' : 'success',
|
|
||||||
description: isError
|
|
||||||
? 'Webhook returned an error'
|
|
||||||
: 'Webhook successfuly executed',
|
|
||||||
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
|
|
||||||
})
|
|
||||||
const newVariables = block.options.responseVariableMapping.reduce<
|
|
||||||
VariableWithUnknowValue[]
|
|
||||||
>((newVariables, varMapping) => {
|
|
||||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
|
||||||
const existingVariable = variables.find(byId(varMapping.variableId))
|
|
||||||
if (!existingVariable) return newVariables
|
|
||||||
const func = Function(
|
|
||||||
'data',
|
|
||||||
`return data.${parseVariables(variables)(varMapping?.bodyPath)}`
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
const value: unknown = func(data)
|
|
||||||
updateVariableValue(existingVariable?.id, value)
|
|
||||||
return [...newVariables, { ...existingVariable, value }]
|
|
||||||
} catch (err) {
|
|
||||||
return newVariables
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
updateVariables(newVariables)
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendEmail = async (
|
|
||||||
block: SendEmailBlock,
|
|
||||||
{
|
|
||||||
variables,
|
|
||||||
apiHost,
|
|
||||||
isPreview,
|
|
||||||
onNewLog,
|
|
||||||
resultId,
|
|
||||||
typebotId,
|
|
||||||
resultValues,
|
|
||||||
}: IntegrationContext
|
|
||||||
) => {
|
|
||||||
if (isPreview) {
|
|
||||||
onNewLog({
|
|
||||||
status: 'info',
|
|
||||||
description: 'Emails are not sent in preview mode',
|
|
||||||
details: null,
|
|
||||||
})
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
const { options } = block
|
|
||||||
const { error } = await sendRequest({
|
|
||||||
url: `${apiHost}/api/typebots/${typebotId}/integrations/email?resultId=${resultId}`,
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
credentialsId: options.credentialsId,
|
|
||||||
recipients: options.recipients.map(parseVariables(variables)),
|
|
||||||
subject: parseVariables(variables)(options.subject ?? ''),
|
|
||||||
body: parseVariables(variables)(options.body ?? ''),
|
|
||||||
cc: (options.cc ?? []).map(parseVariables(variables)),
|
|
||||||
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
|
||||||
replyTo: options.replyTo
|
|
||||||
? parseVariables(variables)(options.replyTo)
|
|
||||||
: undefined,
|
|
||||||
fileUrls: variables.find(byId(options.attachmentsVariableId))?.value,
|
|
||||||
isCustomBody: options.isCustomBody,
|
|
||||||
isBodyCode: options.isBodyCode,
|
|
||||||
resultValues,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
onNewLog(
|
|
||||||
parseLog(error, 'Succesfully sent an email', 'Failed to send an email')
|
|
||||||
)
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseLog = (
|
|
||||||
error: Error | undefined,
|
|
||||||
successMessage: string,
|
|
||||||
errorMessage: string
|
|
||||||
): Omit<Log, 'id' | 'createdAt' | 'resultId'> => ({
|
|
||||||
status: error ? 'error' : 'success',
|
|
||||||
description: error ? errorMessage : successMessage,
|
|
||||||
details: (error && JSON.stringify(error, null, 2).substring(0, 1000)) ?? null,
|
|
||||||
})
|
|
@ -1,262 +0,0 @@
|
|||||||
import { TypebotViewerProps } from 'components/TypebotViewer'
|
|
||||||
import { LinkedTypebot } from 'contexts/TypebotContext'
|
|
||||||
import { Log } from 'db'
|
|
||||||
import {
|
|
||||||
LogicBlock,
|
|
||||||
LogicBlockType,
|
|
||||||
LogicalOperator,
|
|
||||||
ConditionBlock,
|
|
||||||
Variable,
|
|
||||||
ComparisonOperators,
|
|
||||||
SetVariableBlock,
|
|
||||||
RedirectBlock,
|
|
||||||
Comparison,
|
|
||||||
CodeBlock,
|
|
||||||
TypebotLinkBlock,
|
|
||||||
PublicTypebot,
|
|
||||||
Typebot,
|
|
||||||
Edge,
|
|
||||||
VariableWithUnknowValue,
|
|
||||||
} from 'models'
|
|
||||||
import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
|
|
||||||
import { sendEventToParent } from './chat'
|
|
||||||
import { isEmbedded, sanitizeUrl } from './utils'
|
|
||||||
import { parseCorrectValueType, parseVariables } from './variable'
|
|
||||||
|
|
||||||
type EdgeId = string
|
|
||||||
|
|
||||||
type LogicContext = {
|
|
||||||
isPreview: boolean
|
|
||||||
apiHost: string
|
|
||||||
typebot: TypebotViewerProps['typebot']
|
|
||||||
linkedTypebots: LinkedTypebot[]
|
|
||||||
currentTypebotId: string
|
|
||||||
pushEdgeIdInLinkedTypebotQueue: (bot: {
|
|
||||||
edgeId: string
|
|
||||||
typebotId: string
|
|
||||||
}) => void
|
|
||||||
setCurrentTypebotId: (id: string) => void
|
|
||||||
updateVariableValue: (variableId: string, value: unknown) => void
|
|
||||||
updateVariables: (variables: VariableWithUnknowValue[]) => void
|
|
||||||
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
|
|
||||||
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
|
||||||
createEdge: (edge: Edge) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const executeLogic = async (
|
|
||||||
block: LogicBlock,
|
|
||||||
context: LogicContext
|
|
||||||
): Promise<{
|
|
||||||
nextEdgeId?: EdgeId
|
|
||||||
linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot
|
|
||||||
}> => {
|
|
||||||
switch (block.type) {
|
|
||||||
case LogicBlockType.SET_VARIABLE:
|
|
||||||
return { nextEdgeId: executeSetVariable(block, context) }
|
|
||||||
case LogicBlockType.CONDITION:
|
|
||||||
return { nextEdgeId: executeCondition(block, context) }
|
|
||||||
case LogicBlockType.REDIRECT:
|
|
||||||
return { nextEdgeId: executeRedirect(block, context) }
|
|
||||||
case LogicBlockType.CODE:
|
|
||||||
return { nextEdgeId: await executeCode(block, context) }
|
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
|
||||||
return await executeTypebotLink(block, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeSetVariable = (
|
|
||||||
block: SetVariableBlock,
|
|
||||||
{ typebot: { variables }, updateVariableValue, updateVariables }: LogicContext
|
|
||||||
): EdgeId | undefined => {
|
|
||||||
if (!block.options?.variableId) return block.outgoingEdgeId
|
|
||||||
const evaluatedExpression = block.options.expressionToEvaluate
|
|
||||||
? evaluateSetVariableExpression(variables)(
|
|
||||||
block.options.expressionToEvaluate
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
const existingVariable = variables.find(byId(block.options.variableId))
|
|
||||||
if (!existingVariable) return block.outgoingEdgeId
|
|
||||||
updateVariableValue(existingVariable.id, evaluatedExpression)
|
|
||||||
updateVariables([{ ...existingVariable, value: evaluatedExpression }])
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluateSetVariableExpression =
|
|
||||||
(variables: Variable[]) =>
|
|
||||||
(str: string): unknown => {
|
|
||||||
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
|
||||||
str.includes('return ') ? str : `return ${str}`
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
const func = Function(...variables.map((v) => v.id), evaluating)
|
|
||||||
return func(...variables.map((v) => parseCorrectValueType(v.value)))
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`Evaluating: ${evaluating}`, err)
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeCondition = (
|
|
||||||
block: ConditionBlock,
|
|
||||||
{ typebot: { variables } }: LogicContext
|
|
||||||
): EdgeId | undefined => {
|
|
||||||
const { content } = block.items[0]
|
|
||||||
const isConditionPassed =
|
|
||||||
content.logicalOperator === LogicalOperator.AND
|
|
||||||
? content.comparisons.every(executeComparison(variables))
|
|
||||||
: content.comparisons.some(executeComparison(variables))
|
|
||||||
return isConditionPassed
|
|
||||||
? block.items[0].outgoingEdgeId
|
|
||||||
: block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeComparison =
|
|
||||||
(variables: Variable[]) => (comparison: Comparison) => {
|
|
||||||
if (!comparison?.variableId) return false
|
|
||||||
const inputValue = (
|
|
||||||
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
|
||||||
).trim()
|
|
||||||
const value = parseVariables(variables)(comparison.value).trim()
|
|
||||||
if (isNotDefined(value)) return false
|
|
||||||
switch (comparison.comparisonOperator) {
|
|
||||||
case ComparisonOperators.CONTAINS: {
|
|
||||||
return inputValue.toLowerCase().includes(value.toLowerCase())
|
|
||||||
}
|
|
||||||
case ComparisonOperators.EQUAL: {
|
|
||||||
return inputValue === value
|
|
||||||
}
|
|
||||||
case ComparisonOperators.NOT_EQUAL: {
|
|
||||||
return inputValue !== value
|
|
||||||
}
|
|
||||||
case ComparisonOperators.GREATER: {
|
|
||||||
return parseFloat(inputValue) > parseFloat(value)
|
|
||||||
}
|
|
||||||
case ComparisonOperators.LESS: {
|
|
||||||
return parseFloat(inputValue) < parseFloat(value)
|
|
||||||
}
|
|
||||||
case ComparisonOperators.IS_SET: {
|
|
||||||
return isDefined(inputValue) && inputValue.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeRedirect = (
|
|
||||||
block: RedirectBlock,
|
|
||||||
{ typebot: { variables } }: LogicContext
|
|
||||||
): EdgeId | undefined => {
|
|
||||||
if (!block.options?.url) return block.outgoingEdgeId
|
|
||||||
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
|
||||||
const isEmbedded = window.parent && window.location !== window.top?.location
|
|
||||||
if (isEmbedded) {
|
|
||||||
if (!block.options.isNewTab)
|
|
||||||
return ((window.top as Window).location.href = formattedUrl)
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.open(formattedUrl)
|
|
||||||
} catch (err) {
|
|
||||||
sendEventToParent({ redirectUrl: formattedUrl })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self')
|
|
||||||
}
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeCode = async (
|
|
||||||
block: CodeBlock,
|
|
||||||
{ typebot: { variables } }: LogicContext
|
|
||||||
) => {
|
|
||||||
if (!block.options.content) return
|
|
||||||
if (block.options.shouldExecuteInParentContext && isEmbedded) {
|
|
||||||
sendEventToParent({
|
|
||||||
codeToExecute: parseVariables(variables)(block.options.content),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const func = Function(
|
|
||||||
...variables.map((v) => v.id),
|
|
||||||
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
await func(...variables.map((v) => parseCorrectValueType(v.value)))
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return block.outgoingEdgeId
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeTypebotLink = async (
|
|
||||||
block: TypebotLinkBlock,
|
|
||||||
context: LogicContext
|
|
||||||
): Promise<{
|
|
||||||
nextEdgeId?: EdgeId
|
|
||||||
linkedTypebot?: PublicTypebot | LinkedTypebot
|
|
||||||
}> => {
|
|
||||||
const {
|
|
||||||
typebot,
|
|
||||||
linkedTypebots,
|
|
||||||
onNewLog,
|
|
||||||
createEdge,
|
|
||||||
setCurrentTypebotId,
|
|
||||||
pushEdgeIdInLinkedTypebotQueue,
|
|
||||||
currentTypebotId,
|
|
||||||
} = context
|
|
||||||
const linkedTypebot = (
|
|
||||||
block.options.typebotId === 'current'
|
|
||||||
? typebot
|
|
||||||
: [typebot, ...linkedTypebots].find(byId(block.options.typebotId)) ??
|
|
||||||
(await fetchAndInjectTypebot(block, context))
|
|
||||||
) as PublicTypebot | LinkedTypebot | undefined
|
|
||||||
if (!linkedTypebot) {
|
|
||||||
onNewLog({
|
|
||||||
status: 'error',
|
|
||||||
description: 'Failed to link typebot',
|
|
||||||
details: '',
|
|
||||||
})
|
|
||||||
return { nextEdgeId: block.outgoingEdgeId }
|
|
||||||
}
|
|
||||||
if (block.outgoingEdgeId)
|
|
||||||
pushEdgeIdInLinkedTypebotQueue({
|
|
||||||
edgeId: block.outgoingEdgeId,
|
|
||||||
typebotId: currentTypebotId,
|
|
||||||
})
|
|
||||||
setCurrentTypebotId(
|
|
||||||
'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id
|
|
||||||
)
|
|
||||||
const nextGroupId =
|
|
||||||
block.options.groupId ??
|
|
||||||
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
|
||||||
?.id
|
|
||||||
if (!nextGroupId) return { nextEdgeId: block.outgoingEdgeId }
|
|
||||||
const newEdge: Edge = {
|
|
||||||
id: (Math.random() * 1000).toString(),
|
|
||||||
from: { blockId: '', groupId: '' },
|
|
||||||
to: {
|
|
||||||
groupId: nextGroupId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
createEdge(newEdge)
|
|
||||||
return {
|
|
||||||
nextEdgeId: newEdge.id,
|
|
||||||
linkedTypebot: {
|
|
||||||
...linkedTypebot,
|
|
||||||
edges: [...linkedTypebot.edges, newEdge],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchAndInjectTypebot = async (
|
|
||||||
block: TypebotLinkBlock,
|
|
||||||
{ apiHost, injectLinkedTypebot, isPreview }: LogicContext
|
|
||||||
): Promise<LinkedTypebot | undefined> => {
|
|
||||||
const { data, error } = isPreview
|
|
||||||
? await sendRequest<{ typebot: Typebot }>(
|
|
||||||
`/api/typebots/${block.options.typebotId}`
|
|
||||||
)
|
|
||||||
: await sendRequest<{ typebot: PublicTypebot }>(
|
|
||||||
`${apiHost}/api/publicTypebots/${block.options.typebotId}`
|
|
||||||
)
|
|
||||||
if (!data || error) return
|
|
||||||
return injectLinkedTypebot(data.typebot)
|
|
||||||
}
|
|
53
packages/bot-engine/src/types.ts
Normal file
53
packages/bot-engine/src/types.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Log } from 'db'
|
||||||
|
import {
|
||||||
|
Edge,
|
||||||
|
Group,
|
||||||
|
PublicTypebot,
|
||||||
|
ResultValues,
|
||||||
|
Typebot,
|
||||||
|
Variable,
|
||||||
|
VariableWithUnknowValue,
|
||||||
|
} from 'models'
|
||||||
|
import { TypebotViewerProps } from './components/TypebotViewer'
|
||||||
|
import { LinkedTypebot } from './providers/TypebotProvider'
|
||||||
|
|
||||||
|
export type InputSubmitContent = {
|
||||||
|
label?: string
|
||||||
|
value: string
|
||||||
|
itemId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EdgeId = string
|
||||||
|
|
||||||
|
export type LogicState = {
|
||||||
|
isPreview: boolean
|
||||||
|
apiHost: string
|
||||||
|
typebot: TypebotViewerProps['typebot']
|
||||||
|
linkedTypebots: LinkedTypebot[]
|
||||||
|
currentTypebotId: string
|
||||||
|
pushEdgeIdInLinkedTypebotQueue: (bot: {
|
||||||
|
edgeId: string
|
||||||
|
typebotId: string
|
||||||
|
}) => void
|
||||||
|
setCurrentTypebotId: (id: string) => void
|
||||||
|
updateVariableValue: (variableId: string, value: unknown) => void
|
||||||
|
updateVariables: (variables: VariableWithUnknowValue[]) => void
|
||||||
|
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
|
||||||
|
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
|
createEdge: (edge: Edge) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntegrationState = {
|
||||||
|
apiHost: string
|
||||||
|
typebotId: string
|
||||||
|
groupId: string
|
||||||
|
blockId: string
|
||||||
|
isPreview: boolean
|
||||||
|
variables: Variable[]
|
||||||
|
resultValues: ResultValues
|
||||||
|
groups: Group[]
|
||||||
|
resultId?: string
|
||||||
|
updateVariables: (variables: VariableWithUnknowValue[]) => void
|
||||||
|
updateVariableValue: (variableId: string, value: unknown) => void
|
||||||
|
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
|
}
|
@ -4,26 +4,10 @@ import {
|
|||||||
InputBlock,
|
InputBlock,
|
||||||
InputBlockType,
|
InputBlockType,
|
||||||
Block,
|
Block,
|
||||||
TypingEmulation,
|
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { isBubbleBlock, isInputBlock } from 'utils'
|
import { isBubbleBlock, isInputBlock } from 'utils'
|
||||||
import type { TypebotPostMessageData } from 'typebot-js'
|
import type { TypebotPostMessageData } from 'typebot-js'
|
||||||
|
|
||||||
export const computeTypingTimeout = (
|
|
||||||
bubbleContent: string,
|
|
||||||
typingSettings: TypingEmulation
|
|
||||||
) => {
|
|
||||||
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
|
||||||
if (wordCount === 0) wordCount = bubbleContent.length
|
|
||||||
const typedWordsPerMinute = typingSettings.speed
|
|
||||||
let typingTimeout = typingSettings.enabled
|
|
||||||
? (wordCount / typedWordsPerMinute) * 60000
|
|
||||||
: 0
|
|
||||||
if (typingTimeout > typingSettings.maxDelay * 1000)
|
|
||||||
typingTimeout = typingSettings.maxDelay * 1000
|
|
||||||
return typingTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getLastChatBlockType = (
|
export const getLastChatBlockType = (
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
): BubbleBlockType | InputBlockType | undefined => {
|
): BubbleBlockType | InputBlockType | undefined => {
|
31
packages/bot-engine/src/utils/executeIntegration.ts
Normal file
31
packages/bot-engine/src/utils/executeIntegration.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot'
|
||||||
|
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics'
|
||||||
|
import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets'
|
||||||
|
import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail'
|
||||||
|
import { executeWebhook } from '@/features/blocks/integrations/webhook'
|
||||||
|
import { IntegrationState } from '@/types'
|
||||||
|
import { IntegrationBlock, IntegrationBlockType } from 'models'
|
||||||
|
|
||||||
|
export const executeIntegration = ({
|
||||||
|
block,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
block: IntegrationBlock
|
||||||
|
context: IntegrationState
|
||||||
|
}): Promise<string | undefined> => {
|
||||||
|
switch (block.type) {
|
||||||
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
|
return executeGoogleSheetBlock(block, context)
|
||||||
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
|
return executeGoogleAnalyticsBlock(block, context)
|
||||||
|
case IntegrationBlockType.ZAPIER:
|
||||||
|
case IntegrationBlockType.MAKE_COM:
|
||||||
|
case IntegrationBlockType.PABBLY_CONNECT:
|
||||||
|
case IntegrationBlockType.WEBHOOK:
|
||||||
|
return executeWebhook(block, context)
|
||||||
|
case IntegrationBlockType.EMAIL:
|
||||||
|
return executeSendEmailBlock(block, context)
|
||||||
|
case IntegrationBlockType.CHATWOOT:
|
||||||
|
return executeChatwootBlock(block, context)
|
||||||
|
}
|
||||||
|
}
|
30
packages/bot-engine/src/utils/executeLogic.ts
Normal file
30
packages/bot-engine/src/utils/executeLogic.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { TypebotViewerProps } from '@/components/TypebotViewer'
|
||||||
|
import { executeCode } from '@/features/blocks/logic/code'
|
||||||
|
import { executeCondition } from '@/features/blocks/logic/condition'
|
||||||
|
import { executeRedirect } from '@/features/blocks/logic/redirect'
|
||||||
|
import { executeSetVariable } from '@/features/blocks/logic/setVariable'
|
||||||
|
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink'
|
||||||
|
import { LinkedTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { EdgeId, LogicState } from '@/types'
|
||||||
|
import { LogicBlock, LogicBlockType } from 'models'
|
||||||
|
|
||||||
|
export const executeLogic = async (
|
||||||
|
block: LogicBlock,
|
||||||
|
context: LogicState
|
||||||
|
): Promise<{
|
||||||
|
nextEdgeId?: EdgeId
|
||||||
|
linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot
|
||||||
|
}> => {
|
||||||
|
switch (block.type) {
|
||||||
|
case LogicBlockType.SET_VARIABLE:
|
||||||
|
return { nextEdgeId: executeSetVariable(block, context) }
|
||||||
|
case LogicBlockType.CONDITION:
|
||||||
|
return { nextEdgeId: executeCondition(block, context) }
|
||||||
|
case LogicBlockType.REDIRECT:
|
||||||
|
return { nextEdgeId: executeRedirect(block, context) }
|
||||||
|
case LogicBlockType.CODE:
|
||||||
|
return { nextEdgeId: await executeCode(block, context) }
|
||||||
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
|
return await executeTypebotLink(block, context)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Log } from 'db'
|
||||||
|
|
||||||
export const sanitizeUrl = (url: string): string =>
|
export const sanitizeUrl = (url: string): string =>
|
||||||
url.startsWith('http') ||
|
url.startsWith('http') ||
|
||||||
url.startsWith('mailto:') ||
|
url.startsWith('mailto:') ||
|
||||||
@ -14,3 +16,13 @@ export const isEmbedded =
|
|||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
window.parent &&
|
window.parent &&
|
||||||
window.location !== window.top?.location
|
window.location !== window.top?.location
|
||||||
|
|
||||||
|
export const parseLog = (
|
||||||
|
error: Error | undefined,
|
||||||
|
successMessage: string,
|
||||||
|
errorMessage: string
|
||||||
|
): Omit<Log, 'id' | 'createdAt' | 'resultId'> => ({
|
||||||
|
status: error ? 'error' : 'success',
|
||||||
|
description: error ? errorMessage : successMessage,
|
||||||
|
details: (error && JSON.stringify(error, null, 2).substring(0, 1000)) ?? null,
|
||||||
|
})
|
@ -1,3 +1,7 @@
|
|||||||
|
import { validateEmail } from '@/features/blocks/inputs/email'
|
||||||
|
import { validatePhoneNumber } from '@/features/blocks/inputs/phone'
|
||||||
|
import { validateUrl } from '@/features/blocks/inputs/url'
|
||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
import {
|
import {
|
||||||
BubbleBlock,
|
BubbleBlock,
|
||||||
BubbleBlockType,
|
BubbleBlockType,
|
||||||
@ -9,14 +13,7 @@ import {
|
|||||||
UrlInputBlock,
|
UrlInputBlock,
|
||||||
Variable,
|
Variable,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { isPossiblePhoneNumber } from 'react-phone-number-input'
|
|
||||||
import { isInputBlock } from 'utils'
|
import { isInputBlock } from 'utils'
|
||||||
import { parseVariables } from './variable'
|
|
||||||
|
|
||||||
const emailRegex =
|
|
||||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
||||||
const urlRegex =
|
|
||||||
/^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/
|
|
||||||
|
|
||||||
export const isInputValid = (
|
export const isInputValid = (
|
||||||
inputValue: string,
|
inputValue: string,
|
||||||
@ -24,11 +21,11 @@ export const isInputValid = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case InputBlockType.EMAIL:
|
case InputBlockType.EMAIL:
|
||||||
return emailRegex.test(inputValue)
|
return validateEmail(inputValue)
|
||||||
case InputBlockType.PHONE:
|
case InputBlockType.PHONE:
|
||||||
return isPossiblePhoneNumber(inputValue)
|
return validatePhoneNumber(inputValue)
|
||||||
case InputBlockType.URL:
|
case InputBlockType.URL:
|
||||||
return urlRegex.test(inputValue)
|
return validateUrl(inputValue)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -63,30 +60,3 @@ export const parseRetryBlock = (
|
|||||||
outgoingEdgeId: newEdge.id,
|
outgoingEdgeId: newEdge.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseReadableDate = ({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
hasTime,
|
|
||||||
isRange,
|
|
||||||
}: {
|
|
||||||
from: string
|
|
||||||
to: string
|
|
||||||
hasTime?: boolean
|
|
||||||
isRange?: boolean
|
|
||||||
}) => {
|
|
||||||
const currentLocale = window.navigator.language
|
|
||||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: hasTime ? '2-digit' : undefined,
|
|
||||||
minute: hasTime ? '2-digit' : undefined,
|
|
||||||
}
|
|
||||||
const fromReadable = new Date(from).toLocaleString(
|
|
||||||
currentLocale,
|
|
||||||
formatOptions
|
|
||||||
)
|
|
||||||
const toReadable = new Date(to).toLocaleString(currentLocale, formatOptions)
|
|
||||||
return `${fromReadable}${isRange ? ` to ${toReadable}` : ''}`
|
|
||||||
}
|
|
@ -15,7 +15,10 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"baseUrl": "./src",
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
"downlevelIteration": true
|
"downlevelIteration": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user