♻️ (bot) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
a5c8a8a95c
commit
972094425a
@ -30,5 +30,19 @@ module.exports = {
|
||||
'prettier/prettier': 'error',
|
||||
'react/display-name': [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 { useAnswers } from '../../../contexts/AnswersContext'
|
||||
import { useAnswers } from '../../../providers/AnswersProvider'
|
||||
import { InputBlock, InputBlockType } from 'models'
|
||||
import { GuestBubble } from './bubbles/GuestBubble'
|
||||
import { TextForm } from './inputs/TextForm'
|
||||
import { byId } from 'utils'
|
||||
import { DateForm } from './inputs/DateForm'
|
||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { parseVariables } from '../../../services/variable'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
import { PaymentForm } from './inputs/PaymentForm'
|
||||
import { RatingForm } from './inputs/RatingForm'
|
||||
import { FileUploadForm } from './inputs/FileUploadForm'
|
||||
|
||||
export type InputSubmitContent = {
|
||||
label?: string
|
||||
value: string
|
||||
itemId?: string
|
||||
}
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { isInputValid } from '@/utils/inputs'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { TextInput } from '@/features/blocks/inputs/textInput'
|
||||
import { NumberInput } from '@/features/blocks/inputs/number'
|
||||
import { EmailInput } from '@/features/blocks/inputs/email'
|
||||
import { UrlInput } from '@/features/blocks/inputs/url'
|
||||
import { PhoneInput } from '@/features/blocks/inputs/phone'
|
||||
import { DateForm } from '@/features/blocks/inputs/date'
|
||||
import { ChoiceForm } from '@/features/blocks/inputs/buttons'
|
||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||
import { RatingForm } from '@/features/blocks/inputs/rating'
|
||||
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
|
||||
|
||||
export const InputChatBlock = ({
|
||||
block,
|
||||
@ -111,12 +110,44 @@ const Input = ({
|
||||
}) => {
|
||||
switch (block.type) {
|
||||
case InputBlockType.TEXT:
|
||||
return (
|
||||
<TextInput
|
||||
block={block}
|
||||
onSubmit={onSubmit}
|
||||
defaultValue={defaultValue}
|
||||
hasGuestAvatar={hasGuestAvatar}
|
||||
/>
|
||||
)
|
||||
case InputBlockType.NUMBER:
|
||||
return (
|
||||
<NumberInput
|
||||
block={block}
|
||||
onSubmit={onSubmit}
|
||||
defaultValue={defaultValue}
|
||||
hasGuestAvatar={hasGuestAvatar}
|
||||
/>
|
||||
)
|
||||
case InputBlockType.EMAIL:
|
||||
return (
|
||||
<EmailInput
|
||||
block={block}
|
||||
onSubmit={onSubmit}
|
||||
defaultValue={defaultValue}
|
||||
hasGuestAvatar={hasGuestAvatar}
|
||||
/>
|
||||
)
|
||||
case InputBlockType.URL:
|
||||
return (
|
||||
<UrlInput
|
||||
block={block}
|
||||
onSubmit={onSubmit}
|
||||
defaultValue={defaultValue}
|
||||
hasGuestAvatar={hasGuestAvatar}
|
||||
/>
|
||||
)
|
||||
case InputBlockType.PHONE:
|
||||
return (
|
||||
<TextForm
|
||||
<PhoneInput
|
||||
block={block}
|
||||
onSubmit={onSubmit}
|
||||
defaultValue={defaultValue}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Avatar } from 'components/avatars/Avatar'
|
||||
import { Avatar } from '@/components/avatars/Avatar'
|
||||
import React, { useState } from 'react'
|
||||
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 React from 'react'
|
||||
import { EmbedBubble } from './EmbedBubble'
|
||||
import { ImageBubble } from './ImageBubble'
|
||||
import { TextBubble } from './TextBubble'
|
||||
import { VideoBubble } from './VideoBubble'
|
||||
|
||||
type Props = {
|
||||
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 { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { LinkedTypebot, useTypebot } from '../../contexts/TypebotContext'
|
||||
import { LinkedTypebot, useTypebot } from '../../providers/TypebotProvider'
|
||||
import {
|
||||
isBubbleBlock,
|
||||
isBubbleBlockType,
|
||||
@ -12,11 +12,6 @@ import {
|
||||
isLogicBlock,
|
||||
byId,
|
||||
} 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 {
|
||||
BubbleBlock,
|
||||
InputBlock,
|
||||
@ -24,10 +19,16 @@ import {
|
||||
PublicTypebot,
|
||||
Block,
|
||||
} from 'models'
|
||||
import { useChat } from 'contexts/ChatContext'
|
||||
import { getLastChatBlockType } from 'services/chat'
|
||||
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 = {
|
||||
blocks: Block[]
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { ChatGroup } from './ChatGroup'
|
||||
import { useFrame } from 'react-frame-component'
|
||||
import { setCssVariablesValue } from '../services/theme'
|
||||
import { useAnswers } from '../contexts/AnswersContext'
|
||||
import { useAnswers } from '../providers/AnswersProvider'
|
||||
import { Group, Edge, PublicTypebot, Theme, VariableWithValue } from 'models'
|
||||
import { byId, isDefined, isInputBlock, isNotDefined } from 'utils'
|
||||
import { animateScroll as scroll } from 'react-scroll'
|
||||
import { LinkedTypebot, useTypebot } from 'contexts/TypebotContext'
|
||||
import { ChatContext } from 'contexts/ChatContext'
|
||||
import { LinkedTypebot, useTypebot } from '@/providers/TypebotProvider'
|
||||
import { setCssVariablesValue } from '@/features/theme'
|
||||
import { ChatProvider } from '@/providers/ChatProvider'
|
||||
|
||||
type Props = {
|
||||
theme: Theme
|
||||
@ -139,7 +138,7 @@ export const ConversationContainer = ({
|
||||
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"
|
||||
>
|
||||
<ChatContext onScroll={autoScrollToBottom}>
|
||||
<ChatProvider onScroll={autoScrollToBottom}>
|
||||
{displayedGroups.map((displayedGroup, idx) => {
|
||||
const groupAfter = displayedGroups[idx + 1]
|
||||
const groupAfterStartsWithInput =
|
||||
@ -158,7 +157,7 @@ export const ConversationContainer = ({
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ChatContext>
|
||||
</ChatProvider>
|
||||
|
||||
{/* We use a block to simulate padding because it makes iOS scroll flicker */}
|
||||
<div className="w-full h-32" ref={bottomAnchor} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { SVGProps } from 'react'
|
||||
import { SendIcon } from '../../../../assets/icons'
|
||||
import { SendIcon } from './icons'
|
||||
|
||||
type SendButtonProps = {
|
||||
label: string
|
@ -1,11 +1,11 @@
|
||||
import React, { CSSProperties, useMemo } from 'react'
|
||||
import { TypebotContext } from '../contexts/TypebotContext'
|
||||
import { TypebotProvider } from '../providers/TypebotProvider'
|
||||
import Frame from 'react-frame-component'
|
||||
import styles from '../assets/style.css'
|
||||
import importantStyles from '../assets/importantStyles.css'
|
||||
import phoneSyle from '../assets/phone.css'
|
||||
import { ConversationContainer } from './ConversationContainer'
|
||||
import { AnswersContext } from '../contexts/AnswersContext'
|
||||
import { AnswersProvider } from '../providers/AnswersProvider'
|
||||
import {
|
||||
Answer,
|
||||
BackgroundType,
|
||||
@ -89,14 +89,14 @@ export const TypebotViewer = ({
|
||||
}:wght@300;400;600&display=swap');`,
|
||||
}}
|
||||
/>
|
||||
<TypebotContext
|
||||
<TypebotProvider
|
||||
typebot={typebot}
|
||||
apiHost={apiHost}
|
||||
isPreview={isPreview}
|
||||
onNewLog={handleNewLog}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<AnswersContext
|
||||
<AnswersProvider
|
||||
resultId={resultId}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
onVariablesUpdated={onVariablesUpdated}
|
||||
@ -120,8 +120,8 @@ export const TypebotViewer = ({
|
||||
</div>
|
||||
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
|
||||
</div>
|
||||
</AnswersContext>
|
||||
</TypebotContext>
|
||||
</AnswersProvider>
|
||||
</TypebotProvider>
|
||||
</Frame>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
export const TypingContent = (): JSX.Element => (
|
||||
export const TypingBubble = (): JSX.Element => (
|
||||
<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 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 { EmbedBubbleBlock } from 'models'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { TypingBubble } from '../../../../../components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
|
||||
type Props = {
|
||||
block: EmbedBubbleBlock
|
||||
@ -56,7 +56,7 @@ export const EmbedBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
height: isTyping ? '2rem' : '100%',
|
||||
}}
|
||||
>
|
||||
{isTyping ? <TypingContent /> : <></>}
|
||||
{isTyping ? <TypingBubble /> : <></>}
|
||||
</div>
|
||||
<iframe
|
||||
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 { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { ImageBubbleBlock } from 'models'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
block: ImageBubbleBlock
|
||||
@ -61,7 +61,7 @@ export const ImageBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
height: isTyping ? '2rem' : '100%',
|
||||
}}
|
||||
>
|
||||
{isTyping ? <TypingContent /> : <></>}
|
||||
{isTyping ? <TypingBubble /> : null}
|
||||
</div>
|
||||
<img
|
||||
ref={image}
|
@ -0,0 +1 @@
|
||||
export { ImageBubble } from './components/ImageBubble'
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { BubbleBlockType, TextBubbleBlock } from 'models'
|
||||
import { computeTypingTimeout } from 'services/chat'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { computeTypingDuration } from '../utils/computeTypingDuration'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
|
||||
type Props = {
|
||||
block: TextBubbleBlock
|
||||
@ -29,7 +29,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
const typingTimeout = computeTypingTimeout(
|
||||
const typingTimeout = computeTypingDuration(
|
||||
block.content.plainText,
|
||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||
)
|
||||
@ -58,7 +58,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
}}
|
||||
data-testid="host-bubble"
|
||||
>
|
||||
{isTyping ? <TypingContent /> : <></>}
|
||||
{isTyping ? <TypingBubble /> : null}
|
||||
</div>
|
||||
{block.type === BubbleBlockType.TEXT && (
|
||||
<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 { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import {
|
||||
Variable,
|
||||
VideoBubbleContent,
|
||||
VideoBubbleContentType,
|
||||
VideoBubbleBlock,
|
||||
} from 'models'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
block: VideoBubbleBlock
|
||||
@ -54,7 +54,7 @@ export const VideoBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
height: isTyping ? '2rem' : '100%',
|
||||
}}
|
||||
>
|
||||
{isTyping ? <TypingContent /> : <></>}
|
||||
{isTyping ? <TypingBubble /> : <></>}
|
||||
</div>
|
||||
<VideoContent
|
||||
content={block.content}
|
@ -0,0 +1 @@
|
||||
export { VideoBubble } from './components/VideoBubble'
|
@ -1,10 +1,10 @@
|
||||
import { useAnswers } from 'contexts/AnswersContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { useAnswers } from '@/providers/AnswersProvider'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { ChoiceInputBlock } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { InputSubmitContent } from '../InputChatBlock'
|
||||
import { SendButton } from './SendButton'
|
||||
import { SendButton } from '../../../../../components/SendButton'
|
||||
|
||||
type ChoiceFormProps = {
|
||||
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 React, { useState } from 'react'
|
||||
import { parseReadableDate } from 'services/inputs'
|
||||
import { InputSubmitContent } from '../InputChatBlock'
|
||||
import { SendButton } from './SendButton'
|
||||
import { parseReadableDate } from '../utils/parseReadableDate'
|
||||
|
||||
type DateInputProps = {
|
||||
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 { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Spinner, SendButton } from '@/components/SendButton'
|
||||
import { useAnswers } from '@/providers/AnswersProvider'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { FileInputBlock } from 'models'
|
||||
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
||||
import { uploadFiles } from 'utils'
|
||||
import { InputSubmitContent } from '../InputChatBlock'
|
||||
import { SendButton, Spinner } from './SendButton'
|
||||
|
||||
type Props = {
|
||||
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 { useStripe, useElements, PaymentElement } 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 { SendButton, Spinner } from '../SendButton'
|
||||
import { SendButton, Spinner } from '@/components/SendButton'
|
||||
import { useFrame } from 'react-frame-component'
|
||||
import { initStripe } from '../../../../../../lib/stripe'
|
||||
import { parseVariables } from 'services/variable'
|
||||
import { useChat } from 'contexts/ChatContext'
|
||||
import { initStripe } from '@/lib/stripe'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { useChat } from '@/providers/ChatProvider'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery'
|
||||
|
||||
type Props = {
|
||||
options: PaymentInputOptions
|
||||
@ -30,7 +30,7 @@ export const StripePaymentForm = ({ options, onSuccess }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const { data, error } = await createPaymentIntent({
|
||||
const { data, error } = await createPaymentIntentQuery({
|
||||
apiHost,
|
||||
isPreview,
|
||||
variables,
|
@ -0,0 +1 @@
|
||||
export { PaymentForm } from './components/PaymentForm/'
|
@ -1,7 +1,7 @@
|
||||
import { PaymentInputOptions, Variable } from 'models'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const createPaymentIntent = ({
|
||||
export const createPaymentIntentQuery = ({
|
||||
apiHost,
|
||||
isPreview,
|
||||
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 React, { FormEvent, useState } from 'react'
|
||||
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
||||
import { InputSubmitContent } from '../InputChatBlock'
|
||||
import { SendButton } from './SendButton'
|
||||
import { SendButton } from '../../../../../components/SendButton'
|
||||
|
||||
type Props = {
|
||||
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 {
|
||||
EmailInputBlock,
|
||||
InputBlockType,
|
||||
NumberInputBlock,
|
||||
PhoneNumberInputBlock,
|
||||
TextInputBlock,
|
||||
UrlInputBlock,
|
||||
} from 'models'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { InputSubmitContent } from '../../InputChatBlock'
|
||||
import { SendButton } from '../SendButton'
|
||||
import { TextInput } from './TextInput'
|
||||
import { ShortTextInput } from '@/components/inputs/ShortTextInput'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { UrlInputBlock } from 'models'
|
||||
import React, { MutableRefObject, useRef, useState } from 'react'
|
||||
|
||||
type TextFormProps = {
|
||||
block:
|
||||
| TextInputBlock
|
||||
| EmailInputBlock
|
||||
| NumberInputBlock
|
||||
| UrlInputBlock
|
||||
| PhoneNumberInputBlock
|
||||
type UrlInputProps = {
|
||||
block: UrlInputBlock
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
defaultValue?: string
|
||||
hasGuestAvatar: boolean
|
||||
}
|
||||
|
||||
export const TextForm = ({
|
||||
export const UrlInput = ({
|
||||
block,
|
||||
onSubmit,
|
||||
defaultValue,
|
||||
hasGuestAvatar,
|
||||
}: TextFormProps) => {
|
||||
}: UrlInputProps) => {
|
||||
const [inputValue, setInputValue] = useState(defaultValue ?? '')
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const isLongText = block.type === InputBlockType.TEXT && block.options?.isLong
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||
|
||||
const handleChange = (inputValue: string) => {
|
||||
if (block.type === InputBlockType.URL && !inputValue.startsWith('https://'))
|
||||
if (!inputValue.startsWith('https://'))
|
||||
return inputValue === 'https:/'
|
||||
? undefined
|
||||
: setInputValue(`https://${inputValue}`)
|
||||
|
||||
setInputValue(inputValue)
|
||||
}
|
||||
|
||||
@ -51,7 +36,6 @@ export const TextForm = ({
|
||||
}
|
||||
|
||||
const submitWhenEnter = (e: React.KeyboardEvent) => {
|
||||
if (block.type === InputBlockType.TEXT && block.options.isLong) return
|
||||
if (e.key === 'Enter') submit()
|
||||
}
|
||||
|
||||
@ -63,15 +47,17 @@ export const TextForm = ({
|
||||
data-testid="input"
|
||||
style={{
|
||||
marginRight: hasGuestAvatar ? '50px' : '0.5rem',
|
||||
maxWidth: isLongText ? undefined : '350px',
|
||||
maxWidth: '350px',
|
||||
}}
|
||||
onKeyDown={submitWhenEnter}
|
||||
>
|
||||
<TextInput
|
||||
inputRef={inputRef}
|
||||
block={block}
|
||||
onChange={handleChange}
|
||||
<ShortTextInput
|
||||
ref={inputRef as MutableRefObject<HTMLInputElement>}
|
||||
value={inputValue}
|
||||
placeholder={block.options?.labels?.placeholder ?? 'Type your URL...'}
|
||||
onChange={handleChange}
|
||||
type="url"
|
||||
autoComplete="url"
|
||||
/>
|
||||
<SendButton
|
||||
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 { 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']) => `
|
||||
window.$chatwoot.setUser("${user?.id ?? ''}", {
|
||||
@ -47,9 +47,9 @@ if (window.$chatwoot) {
|
||||
})(document, "script");
|
||||
}`
|
||||
|
||||
export const openChatwootWidget = async (
|
||||
export const executeChatwootBlock = async (
|
||||
block: ChatwootBlock,
|
||||
{ variables, isPreview, onNewLog }: IntegrationContext
|
||||
{ variables, isPreview, onNewLog }: IntegrationState
|
||||
) => {
|
||||
if (isPreview) {
|
||||
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 { parseVariables } from './services/variable'
|
||||
export { parseVariables } from '@/features/variables'
|
||||
export * from 'util'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { safeStringify } from '@/features/variables'
|
||||
import {
|
||||
Answer,
|
||||
ResultValues,
|
||||
@ -5,7 +6,6 @@ import {
|
||||
VariableWithValue,
|
||||
} from 'models'
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
import { safeStringify } from 'services/variable'
|
||||
|
||||
const answersContext = createContext<{
|
||||
resultId?: string
|
||||
@ -18,7 +18,7 @@ const answersContext = createContext<{
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const AnswersContext = ({
|
||||
export const AnswersProvider = ({
|
||||
children,
|
||||
resultId,
|
||||
onNewAnswer,
|
@ -6,7 +6,7 @@ const chatContext = createContext<{
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const ChatContext = ({
|
||||
export const ChatProvider = ({
|
||||
children,
|
||||
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 { Edge, PublicTypebot, Typebot } from 'models'
|
||||
import React, {
|
||||
@ -8,8 +10,6 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { sendEventToParent } from 'services/chat'
|
||||
import { safeStringify } from 'services/variable'
|
||||
|
||||
export type LinkedTypebot = Pick<
|
||||
PublicTypebot | Typebot,
|
||||
@ -43,7 +43,7 @@ const typebotContext = createContext<{
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const TypebotContext = ({
|
||||
export const TypebotProvider = ({
|
||||
children,
|
||||
typebot,
|
||||
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,
|
||||
InputBlockType,
|
||||
Block,
|
||||
TypingEmulation,
|
||||
} from 'models'
|
||||
import { isBubbleBlock, isInputBlock } from 'utils'
|
||||
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 = (
|
||||
blocks: Block[]
|
||||
): 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 =>
|
||||
url.startsWith('http') ||
|
||||
url.startsWith('mailto:') ||
|
||||
@ -14,3 +16,13 @@ export const isEmbedded =
|
||||
typeof window !== 'undefined' &&
|
||||
window.parent &&
|
||||
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 {
|
||||
BubbleBlock,
|
||||
BubbleBlockType,
|
||||
@ -9,14 +13,7 @@ import {
|
||||
UrlInputBlock,
|
||||
Variable,
|
||||
} from 'models'
|
||||
import { isPossiblePhoneNumber } from 'react-phone-number-input'
|
||||
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 = (
|
||||
inputValue: string,
|
||||
@ -24,11 +21,11 @@ export const isInputValid = (
|
||||
): boolean => {
|
||||
switch (type) {
|
||||
case InputBlockType.EMAIL:
|
||||
return emailRegex.test(inputValue)
|
||||
return validateEmail(inputValue)
|
||||
case InputBlockType.PHONE:
|
||||
return isPossiblePhoneNumber(inputValue)
|
||||
return validatePhoneNumber(inputValue)
|
||||
case InputBlockType.URL:
|
||||
return urlRegex.test(inputValue)
|
||||
return validateUrl(inputValue)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -63,30 +60,3 @@ export const parseRetryBlock = (
|
||||
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",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"baseUrl": "./src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"downlevelIteration": true
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user