@@ -180,7 +180,8 @@
|
|||||||
"type": "Typebot link",
|
"type": "Typebot link",
|
||||||
"options": {
|
"options": {
|
||||||
"typebotId": "current",
|
"typebotId": "current",
|
||||||
"groupId": "vLUAPaxKwPF49iZhg4XZYa"
|
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
|
||||||
|
"mergeResults": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -305,7 +306,8 @@
|
|||||||
"groupId": "1GvxCAAEysxJMxrVngud3X",
|
"groupId": "1GvxCAAEysxJMxrVngud3X",
|
||||||
"options": {
|
"options": {
|
||||||
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
|
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
|
||||||
"typebotId": "current"
|
"typebotId": "current",
|
||||||
|
"mergeResults": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -282,7 +282,8 @@
|
|||||||
"type": "Typebot link",
|
"type": "Typebot link",
|
||||||
"options": {
|
"options": {
|
||||||
"typebotId": "current",
|
"typebotId": "current",
|
||||||
"groupId": "cl96ns9qr00043b6ii07bo25o"
|
"groupId": "cl96ns9qr00043b6ii07bo25o",
|
||||||
|
"mergeResults": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -618,7 +618,8 @@
|
|||||||
"groupId": "cl1r15f68005f2e6dvdtal7cp",
|
"groupId": "cl1r15f68005f2e6dvdtal7cp",
|
||||||
"options": {
|
"options": {
|
||||||
"groupId": "cl1r09bc6000h2e6dqml18p4p",
|
"groupId": "cl1r09bc6000h2e6dqml18p4p",
|
||||||
"typebotId": "current"
|
"typebotId": "current",
|
||||||
|
"mergeResults": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export const getLinkedTypebots = authenticatedProcedure
|
|||||||
(typebotIds, block) =>
|
(typebotIds, block) =>
|
||||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||||
isDefined(block.options.typebotId) &&
|
isDefined(block.options.typebotId) &&
|
||||||
!typebotIds.includes(block.options.typebotId)
|
!typebotIds.includes(block.options.typebotId) &&
|
||||||
|
block.options.mergeResults !== false
|
||||||
? [...typebotIds, block.options.typebotId]
|
? [...typebotIds, block.options.typebotId]
|
||||||
: typebotIds,
|
: typebotIds,
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Stack } from '@chakra-ui/react'
|
import { Stack } from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { TypebotLinkOptions } from '@typebot.io/schemas'
|
import { TypebotLinkOptions } from '@typebot.io/schemas'
|
||||||
import { byId } from '@typebot.io/lib'
|
|
||||||
import { GroupsDropdown } from './GroupsDropdown'
|
import { GroupsDropdown } from './GroupsDropdown'
|
||||||
import { TypebotsDropdown } from './TypebotsDropdown'
|
import { TypebotsDropdown } from './TypebotsDropdown'
|
||||||
import { useEffect, useState } from 'react'
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { isNotEmpty } from '@typebot.io/lib'
|
||||||
|
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: TypebotLinkOptions
|
options: TypebotLinkOptions
|
||||||
@@ -12,21 +13,30 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
|
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
|
||||||
const { linkedTypebots, typebot, save } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const [linkedTypebotId, setLinkedTypebotId] = useState(options.typebotId)
|
|
||||||
|
|
||||||
const handleTypebotIdChange = async (
|
const handleTypebotIdChange = async (
|
||||||
typebotId: string | 'current' | undefined
|
typebotId: string | 'current' | undefined
|
||||||
) => onOptionsChange({ ...options, typebotId })
|
) => onOptionsChange({ ...options, typebotId, groupId: undefined })
|
||||||
|
|
||||||
|
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
|
||||||
|
{
|
||||||
|
typebotId: options.typebotId as string,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isNotEmpty(options.typebotId) && options.typebotId !== 'current',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const handleGroupIdChange = (groupId: string | undefined) =>
|
const handleGroupIdChange = (groupId: string | undefined) =>
|
||||||
onOptionsChange({ ...options, groupId })
|
onOptionsChange({ ...options, groupId })
|
||||||
|
|
||||||
useEffect(() => {
|
const updateMergeResults = (mergeResults: boolean) =>
|
||||||
if (linkedTypebotId === options.typebotId) return
|
onOptionsChange({ ...options, mergeResults })
|
||||||
setLinkedTypebotId(options.typebotId)
|
|
||||||
save().then()
|
const isCurrentTypebotSelected =
|
||||||
}, [linkedTypebotId, options.typebotId, save])
|
(typebot && options.typebotId === typebot.id) ||
|
||||||
|
options.typebotId === 'current'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -40,22 +50,30 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{options.typebotId && (
|
{options.typebotId && (
|
||||||
<GroupsDropdown
|
<GroupsDropdown
|
||||||
|
key={options.typebotId}
|
||||||
groups={
|
groups={
|
||||||
typebot &&
|
typebot && isCurrentTypebotSelected
|
||||||
(options.typebotId === typebot.id ||
|
|
||||||
options.typebotId === 'current')
|
|
||||||
? typebot.groups
|
? typebot.groups
|
||||||
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
|
: linkedTypebotData?.typebot?.groups ?? []
|
||||||
}
|
}
|
||||||
groupId={options.groupId}
|
groupId={options.groupId}
|
||||||
onGroupIdSelected={handleGroupIdChange}
|
onGroupIdSelected={handleGroupIdChange}
|
||||||
isLoading={
|
isLoading={
|
||||||
linkedTypebots === undefined &&
|
linkedTypebotData?.typebot === undefined &&
|
||||||
|
options.typebotId !== 'current' &&
|
||||||
typebot &&
|
typebot &&
|
||||||
typebot.id !== options.typebotId
|
typebot.id !== options.typebotId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isCurrentTypebotSelected && (
|
||||||
|
<SwitchWithLabel
|
||||||
|
label="Merge answers"
|
||||||
|
moreInfoContent="If enabled, the answers collected in the linked typebot will be merged with the results of the current typebot."
|
||||||
|
initialValue={options.mergeResults ?? true}
|
||||||
|
onCheckChange={updateMergeResults}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,36 @@ import { TypebotLinkBlock } from '@typebot.io/schemas'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Tag, Text } from '@chakra-ui/react'
|
import { Tag, Text } from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { byId } from '@typebot.io/lib'
|
import { byId, isNotEmpty } from '@typebot.io/lib'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: TypebotLinkBlock
|
block: TypebotLinkBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TypebotLinkNode = ({ block }: Props) => {
|
export const TypebotLinkNode = ({ block }: Props) => {
|
||||||
const { linkedTypebots, typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
|
|
||||||
|
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
|
||||||
|
{
|
||||||
|
typebotId: block.options.typebotId as string,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled:
|
||||||
|
isNotEmpty(block.options.typebotId) &&
|
||||||
|
block.options.typebotId !== 'current',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const isCurrentTypebot =
|
const isCurrentTypebot =
|
||||||
typebot &&
|
typebot &&
|
||||||
(block.options.typebotId === typebot.id ||
|
(block.options.typebotId === typebot.id ||
|
||||||
block.options.typebotId === 'current')
|
block.options.typebotId === 'current')
|
||||||
const linkedTypebot = isCurrentTypebot
|
const linkedTypebot = isCurrentTypebot ? typebot : linkedTypebotData?.typebot
|
||||||
? typebot
|
|
||||||
: linkedTypebots?.find(byId(block.options.typebotId))
|
|
||||||
const blockTitle = linkedTypebot?.groups.find(
|
const blockTitle = linkedTypebot?.groups.find(
|
||||||
byId(block.options.groupId)
|
byId(block.options.groupId)
|
||||||
)?.title
|
)?.title
|
||||||
|
|
||||||
if (!block.options.typebotId)
|
if (!block.options.typebotId)
|
||||||
return <Text color="gray.500">Configure...</Text>
|
return <Text color="gray.500">Configure...</Text>
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -61,7 +61,17 @@ export const TypebotsDropdown = ({
|
|||||||
aria-label="Navigate to typebot"
|
aria-label="Navigate to typebot"
|
||||||
icon={<ExternalLinkIcon />}
|
icon={<ExternalLinkIcon />}
|
||||||
as={Link}
|
as={Link}
|
||||||
href={`/typebots/${typebotId}/edit?parentId=${query.typebotId}`}
|
href={{
|
||||||
|
pathname: '/typebots/[typebotId]/edit',
|
||||||
|
query: {
|
||||||
|
typebotId,
|
||||||
|
parentId: query.parentId
|
||||||
|
? Array.isArray(query.parentId)
|
||||||
|
? query.parentId.concat(query.typebotId?.toString() ?? '')
|
||||||
|
: [query.parentId, query.typebotId?.toString() ?? '']
|
||||||
|
: query.typebotId ?? [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -156,13 +156,22 @@ export const TypebotHeader = () => {
|
|||||||
as={Link}
|
as={Link}
|
||||||
aria-label="Navigate back"
|
aria-label="Navigate back"
|
||||||
icon={<ChevronLeftIcon fontSize={25} />}
|
icon={<ChevronLeftIcon fontSize={25} />}
|
||||||
href={
|
href={{
|
||||||
router.query.parentId
|
pathname: router.query.parentId
|
||||||
? `/typebots/${router.query.parentId}/edit`
|
? '/typebots/[typebotId]/edit'
|
||||||
: typebot?.folderId
|
: typebot?.folderId
|
||||||
? `/typebots/folders/${typebot.folderId}`
|
? '/typebots/folders/[folderId]'
|
||||||
: '/typebots'
|
: '/typebots',
|
||||||
}
|
query: {
|
||||||
|
folderId: typebot?.folderId ?? [],
|
||||||
|
parentId: Array.isArray(router.query.parentId)
|
||||||
|
? router.query.parentId.slice(0, -1)
|
||||||
|
: [],
|
||||||
|
typebotId: Array.isArray(router.query.parentId)
|
||||||
|
? [...router.query.parentId].pop()
|
||||||
|
: router.query.parentId ?? [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
|
import { PublicTypebot, Typebot } from '@typebot.io/schemas'
|
||||||
import { Router, useRouter } from 'next/router'
|
import { Router, useRouter } from 'next/router'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -49,7 +49,6 @@ const typebotContext = createContext<
|
|||||||
{
|
{
|
||||||
typebot?: Typebot
|
typebot?: Typebot
|
||||||
publishedTypebot?: PublicTypebot
|
publishedTypebot?: PublicTypebot
|
||||||
linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
|
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
isPublished: boolean
|
isPublished: boolean
|
||||||
isSavingLoading: boolean
|
isSavingLoading: boolean
|
||||||
@@ -132,36 +131,6 @@ export const TypebotProvider = ({
|
|||||||
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
||||||
] = useUndo<Typebot>(undefined)
|
] = useUndo<Typebot>(undefined)
|
||||||
|
|
||||||
const linkedTypebotIds = useMemo(
|
|
||||||
() =>
|
|
||||||
typebot?.groups
|
|
||||||
.flatMap((group) => group.blocks)
|
|
||||||
.reduce<string[]>(
|
|
||||||
(typebotIds, block) =>
|
|
||||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
|
||||||
isDefined(block.options.typebotId) &&
|
|
||||||
!typebotIds.includes(block.options.typebotId)
|
|
||||||
? [...typebotIds, block.options.typebotId]
|
|
||||||
: typebotIds,
|
|
||||||
[]
|
|
||||||
) ?? [],
|
|
||||||
[typebot?.groups]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
|
|
||||||
{
|
|
||||||
typebotId: typebot?.id as string,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: isDefined(typebot?.id) && linkedTypebotIds.length > 0,
|
|
||||||
onError: (error) =>
|
|
||||||
showToast({
|
|
||||||
title: 'Error while fetching linkedTypebots',
|
|
||||||
description: error.message,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
||||||
if (isFetchingTypebot) return
|
if (isFetchingTypebot) return
|
||||||
@@ -270,7 +239,6 @@ export const TypebotProvider = ({
|
|||||||
value={{
|
value={{
|
||||||
typebot: localTypebot,
|
typebot: localTypebot,
|
||||||
publishedTypebot,
|
publishedTypebot,
|
||||||
linkedTypebots: linkedTypebotsData?.typebots ?? [],
|
|
||||||
isReadOnly: typebotData?.isReadOnly,
|
isReadOnly: typebotData?.isReadOnly,
|
||||||
isSavingLoading: isSaving,
|
isSavingLoading: isSaving,
|
||||||
save: saveTypebot,
|
save: saveTypebot,
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { ResultHeaderCell, ResultWithAnswers } from '@typebot.io/schemas'
|
import {
|
||||||
|
LogicBlockType,
|
||||||
|
ResultHeaderCell,
|
||||||
|
ResultWithAnswers,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import { createContext, ReactNode, useContext, useMemo } from 'react'
|
import { createContext, ReactNode, useContext, useMemo } from 'react'
|
||||||
import { parseResultHeader } from '@typebot.io/lib/results'
|
import { parseResultHeader } from '@typebot.io/lib/results'
|
||||||
import { useTypebot } from '../editor/providers/TypebotProvider'
|
import { useTypebot } from '../editor/providers/TypebotProvider'
|
||||||
import { useResultsQuery } from './hooks/useResultsQuery'
|
import { useResultsQuery } from './hooks/useResultsQuery'
|
||||||
import { TableData } from './types'
|
import { TableData } from './types'
|
||||||
import { convertResultsToTableData } from './helpers/convertResultsToTableData'
|
import { convertResultsToTableData } from './helpers/convertResultsToTableData'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { isDefined } from '@typebot.io/lib/utils'
|
||||||
|
|
||||||
const resultsContext = createContext<{
|
const resultsContext = createContext<{
|
||||||
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
||||||
@@ -32,7 +38,7 @@ export const ResultsProvider = ({
|
|||||||
totalResults: number
|
totalResults: number
|
||||||
onDeleteResults: (totalResultsDeleted: number) => void
|
onDeleteResults: (totalResultsDeleted: number) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { publishedTypebot, linkedTypebots } = useTypebot()
|
const { publishedTypebot } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
|
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
|
||||||
typebotId,
|
typebotId,
|
||||||
@@ -41,6 +47,29 @@ export const ResultsProvider = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const linkedTypebotIds =
|
||||||
|
publishedTypebot?.groups
|
||||||
|
.flatMap((group) => group.blocks)
|
||||||
|
.reduce<string[]>(
|
||||||
|
(typebotIds, block) =>
|
||||||
|
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||||
|
isDefined(block.options.typebotId) &&
|
||||||
|
!typebotIds.includes(block.options.typebotId) &&
|
||||||
|
block.options.mergeResults !== false
|
||||||
|
? [...typebotIds, block.options.typebotId]
|
||||||
|
: typebotIds,
|
||||||
|
[]
|
||||||
|
) ?? []
|
||||||
|
|
||||||
|
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
|
||||||
|
{
|
||||||
|
typebotId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: linkedTypebotIds.length > 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const flatResults = useMemo(
|
const flatResults = useMemo(
|
||||||
() => data?.flatMap((d) => d.results) ?? [],
|
() => data?.flatMap((d) => d.results) ?? [],
|
||||||
[data]
|
[data]
|
||||||
@@ -49,9 +78,9 @@ export const ResultsProvider = ({
|
|||||||
const resultHeader = useMemo(
|
const resultHeader = useMemo(
|
||||||
() =>
|
() =>
|
||||||
publishedTypebot
|
publishedTypebot
|
||||||
? parseResultHeader(publishedTypebot, linkedTypebots)
|
? parseResultHeader(publishedTypebot, linkedTypebotsData?.typebots)
|
||||||
: [],
|
: [],
|
||||||
[linkedTypebots, publishedTypebot]
|
[linkedTypebotsData?.typebots, publishedTypebot]
|
||||||
)
|
)
|
||||||
|
|
||||||
const tableData = useMemo(
|
const tableData = useMemo(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useResults } from '../../ResultsProvider'
|
|||||||
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
|
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
|
||||||
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
|
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
|
||||||
import { parseAccessor } from '../../helpers/parseAccessor'
|
import { parseAccessor } from '../../helpers/parseAccessor'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -30,7 +31,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
||||||
const { typebot, publishedTypebot, linkedTypebots } = useTypebot()
|
const { typebot, publishedTypebot } = useTypebot()
|
||||||
const workspaceId = typebot?.workspaceId
|
const workspaceId = typebot?.workspaceId
|
||||||
const typebotId = typebot?.id
|
const typebotId = typebot?.id
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
@@ -41,6 +42,15 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
|||||||
const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] =
|
const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] =
|
||||||
useState(false)
|
useState(false)
|
||||||
|
|
||||||
|
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
|
||||||
|
{
|
||||||
|
typebotId: typebotId as string,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDefined(typebotId),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const getAllResults = async () => {
|
const getAllResults = async () => {
|
||||||
if (!workspaceId || !typebotId) return []
|
if (!workspaceId || !typebotId) return []
|
||||||
const allResults = []
|
const allResults = []
|
||||||
@@ -71,7 +81,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
|||||||
const results = await getAllResults()
|
const results = await getAllResults()
|
||||||
|
|
||||||
const resultHeader = areDeletedBlocksIncluded
|
const resultHeader = areDeletedBlocksIncluded
|
||||||
? parseResultHeader(publishedTypebot, linkedTypebots, results)
|
? parseResultHeader(
|
||||||
|
publishedTypebot,
|
||||||
|
linkedTypebotsData?.typebots,
|
||||||
|
results
|
||||||
|
)
|
||||||
: existingResultHeader
|
: existingResultHeader
|
||||||
|
|
||||||
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
defaultAbTestOptions,
|
defaultAbTestOptions,
|
||||||
BlockWithItems,
|
BlockWithItems,
|
||||||
|
defaultTypebotLinkOptions,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
|||||||
case LogicBlockType.JUMP:
|
case LogicBlockType.JUMP:
|
||||||
return {}
|
return {}
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return {}
|
return defaultTypebotLinkOptions
|
||||||
case LogicBlockType.AB_TEST:
|
case LogicBlockType.AB_TEST:
|
||||||
return defaultAbTestOptions
|
return defaultAbTestOptions
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { filterChoiceItems } from './filterChoiceItems'
|
|||||||
export const injectVariableValuesInButtonsInputBlock =
|
export const injectVariableValuesInButtonsInputBlock =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (block.options.dynamicVariableId) {
|
if (block.options.dynamicVariableId) {
|
||||||
const variable = state.typebot.variables.find(
|
const variable = variables.find(
|
||||||
(variable) =>
|
(variable) =>
|
||||||
variable.id === block.options.dynamicVariableId &&
|
variable.id === block.options.dynamicVariableId &&
|
||||||
isDefined(variable.value)
|
isDefined(variable.value)
|
||||||
@@ -31,18 +32,17 @@ export const injectVariableValuesInButtonsInputBlock =
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deepParseVariables(state.typebot.variables)(
|
return deepParseVariables(variables)(filterChoiceItems(variables)(block))
|
||||||
filterChoiceItems(state.typebot.variables)(block)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getVariableValue =
|
const getVariableValue =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
(variable: VariableWithValue): (string | null)[] => {
|
(variable: VariableWithValue): (string | null)[] => {
|
||||||
if (!Array.isArray(variable.value)) {
|
if (!Array.isArray(variable.value)) {
|
||||||
const [transformedVariable] = transformStringVariablesToList(
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
state.typebot.variables
|
const [transformedVariable] = transformStringVariablesToList(variables)([
|
||||||
)([variable.id])
|
variable.id,
|
||||||
|
])
|
||||||
updateVariables(state)([transformedVariable])
|
updateVariables(state)([transformedVariable])
|
||||||
return transformedVariable.value as string[]
|
return transformedVariable.value as string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ import Stripe from 'stripe'
|
|||||||
import { decrypt } from '@typebot.io/lib/api/encryption'
|
import { decrypt } from '@typebot.io/lib/api/encryption'
|
||||||
|
|
||||||
export const computePaymentInputRuntimeOptions =
|
export const computePaymentInputRuntimeOptions =
|
||||||
(state: Pick<SessionState, 'result' | 'typebot'>) =>
|
(state: SessionState) => (options: PaymentInputOptions) =>
|
||||||
(options: PaymentInputOptions) =>
|
|
||||||
createStripePaymentIntent(state)(options)
|
createStripePaymentIntent(state)(options)
|
||||||
|
|
||||||
const createStripePaymentIntent =
|
const createStripePaymentIntent =
|
||||||
(state: Pick<SessionState, 'result' | 'typebot'>) =>
|
(state: SessionState) =>
|
||||||
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
||||||
const {
|
const {
|
||||||
result,
|
resultId,
|
||||||
typebot: { variables },
|
typebot: { variables },
|
||||||
} = state
|
} = state.typebotsQueue[0]
|
||||||
const isPreview = !result.id
|
const isPreview = !resultId
|
||||||
if (!options.credentialsId)
|
if (!options.credentialsId)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
SessionState,
|
|
||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
ItemType,
|
ItemType,
|
||||||
PictureChoiceBlock,
|
PictureChoiceBlock,
|
||||||
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||||
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
|
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
|
||||||
|
|
||||||
export const injectVariableValuesInPictureChoiceBlock =
|
export const injectVariableValuesInPictureChoiceBlock =
|
||||||
(variables: SessionState['typebot']['variables']) =>
|
(variables: Variable[]) =>
|
||||||
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
||||||
if (
|
if (
|
||||||
block.options.dynamicItems?.isEnabled &&
|
block.options.dynamicItems?.isEnabled &&
|
||||||
|
|||||||
@@ -70,17 +70,18 @@ if (window.$chatwoot) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const executeChatwootBlock = (
|
export const executeChatwootBlock = (
|
||||||
{ typebot, result }: SessionState,
|
state: SessionState,
|
||||||
block: ChatwootBlock
|
block: ChatwootBlock
|
||||||
): ExecuteIntegrationResponse => {
|
): ExecuteIntegrationResponse => {
|
||||||
|
const { typebot, resultId } = state.typebotsQueue[0]
|
||||||
const chatwootCode =
|
const chatwootCode =
|
||||||
block.options.task === 'Close widget'
|
block.options.task === 'Close widget'
|
||||||
? chatwootCloseCode
|
? chatwootCloseCode
|
||||||
: isDefined(result.id)
|
: isDefined(resultId)
|
||||||
? parseChatwootOpenCode({
|
? parseChatwootOpenCode({
|
||||||
...block.options,
|
...block.options,
|
||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
resultId: result.id,
|
resultId,
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
|||||||
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
|
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const executeGoogleAnalyticsBlock = (
|
export const executeGoogleAnalyticsBlock = (
|
||||||
{ typebot: { variables }, result }: SessionState,
|
state: SessionState,
|
||||||
block: GoogleAnalyticsBlock
|
block: GoogleAnalyticsBlock
|
||||||
): ExecuteIntegrationResponse => {
|
): ExecuteIntegrationResponse => {
|
||||||
if (!result) return { outgoingEdgeId: block.outgoingEdgeId }
|
const { typebot, resultId } = state.typebotsQueue[0]
|
||||||
const googleAnalytics = deepParseVariables(variables, {
|
if (!resultId) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
|
const googleAnalytics = deepParseVariables(typebot.variables, {
|
||||||
guessCorrectTypes: true,
|
guessCorrectTypes: true,
|
||||||
removeEmptyStrings: true,
|
removeEmptyStrings: true,
|
||||||
})(block.options)
|
})(block.options)
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ export const getRow = async (
|
|||||||
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
const { sheetId, cellsToExtract, referenceCell, filter } = deepParseVariables(
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
state.typebot.variables
|
const { sheetId, cellsToExtract, referenceCell, filter } =
|
||||||
)(options)
|
deepParseVariables(variables)(options)
|
||||||
if (!sheetId) return { outgoingEdgeId }
|
if (!sheetId) return { outgoingEdgeId }
|
||||||
|
|
||||||
const variables = state.typebot.variables
|
|
||||||
|
|
||||||
const doc = await getAuthenticatedGoogleDoc({
|
const doc = await getAuthenticatedGoogleDoc({
|
||||||
credentialsId: options.credentialsId,
|
credentialsId: options.credentialsId,
|
||||||
spreadsheetId: options.spreadsheetId,
|
spreadsheetId: options.spreadsheetId,
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
|||||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||||
|
|
||||||
export const insertRow = async (
|
export const insertRow = async (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
{
|
{
|
||||||
outgoingEdgeId,
|
outgoingEdgeId,
|
||||||
options,
|
options,
|
||||||
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
|
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
|
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
|
||||||
|
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
|||||||
import { matchFilter } from './helpers/matchFilter'
|
import { matchFilter } from './helpers/matchFilter'
|
||||||
|
|
||||||
export const updateRow = async (
|
export const updateRow = async (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
{
|
{
|
||||||
outgoingEdgeId,
|
outgoingEdgeId,
|
||||||
options,
|
options,
|
||||||
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
|
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
const { sheetId, referenceCell, filter } =
|
const { sheetId, referenceCell, filter } =
|
||||||
deepParseVariables(variables)(options)
|
deepParseVariables(variables)(options)
|
||||||
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
|
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { Block, BubbleBlockType, SessionState } from '@typebot.io/schemas'
|
import {
|
||||||
|
Block,
|
||||||
|
BubbleBlockType,
|
||||||
|
SessionState,
|
||||||
|
TypebotInSession,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import {
|
import {
|
||||||
ChatCompletionOpenAIOptions,
|
ChatCompletionOpenAIOptions,
|
||||||
OpenAICredentials,
|
OpenAICredentials,
|
||||||
@@ -51,13 +56,16 @@ export const createChatCompletionOpenAI = async (
|
|||||||
credentials.data,
|
credentials.data,
|
||||||
credentials.iv
|
credentials.iv
|
||||||
)) as OpenAICredentials['data']
|
)) as OpenAICredentials['data']
|
||||||
|
|
||||||
|
const { typebot } = newSessionState.typebotsQueue[0]
|
||||||
|
|
||||||
const { variablesTransformedToList, messages } = parseChatCompletionMessages(
|
const { variablesTransformedToList, messages } = parseChatCompletionMessages(
|
||||||
newSessionState.typebot.variables
|
typebot.variables
|
||||||
)(options.messages)
|
)(options.messages)
|
||||||
if (variablesTransformedToList.length > 0)
|
if (variablesTransformedToList.length > 0)
|
||||||
newSessionState = updateVariables(state)(variablesTransformedToList)
|
newSessionState = updateVariables(state)(variablesTransformedToList)
|
||||||
|
|
||||||
const temperature = parseVariableNumber(newSessionState.typebot.variables)(
|
const temperature = parseVariableNumber(typebot.variables)(
|
||||||
options.advancedSettings?.temperature
|
options.advancedSettings?.temperature
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ export const createChatCompletionOpenAI = async (
|
|||||||
isCredentialsV2(credentials) &&
|
isCredentialsV2(credentials) &&
|
||||||
newSessionState.isStreamEnabled
|
newSessionState.isStreamEnabled
|
||||||
) {
|
) {
|
||||||
const assistantMessageVariableName = state.typebot.variables.find(
|
const assistantMessageVariableName = typebot.variables.find(
|
||||||
(variable) =>
|
(variable) =>
|
||||||
options.responseMapping.find(
|
options.responseMapping.find(
|
||||||
(m) => m.valueToExtract === 'Message content'
|
(m) => m.valueToExtract === 'Message content'
|
||||||
@@ -81,9 +89,10 @@ export const createChatCompletionOpenAI = async (
|
|||||||
content?: string
|
content?: string
|
||||||
role: (typeof chatCompletionMessageRoles)[number]
|
role: (typeof chatCompletionMessageRoles)[number]
|
||||||
}[],
|
}[],
|
||||||
displayStream: isNextBubbleMessageWithAssistantMessage(
|
displayStream: isNextBubbleMessageWithAssistantMessage(typebot)(
|
||||||
state.typebot
|
blockId,
|
||||||
)(blockId, assistantMessageVariableName),
|
assistantMessageVariableName
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -117,7 +126,7 @@ export const createChatCompletionOpenAI = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isNextBubbleMessageWithAssistantMessage =
|
const isNextBubbleMessageWithAssistantMessage =
|
||||||
(typebot: SessionState['typebot']) =>
|
(typebot: TypebotInSession) =>
|
||||||
(blockId: string, assistantVariableName?: string): boolean => {
|
(blockId: string, assistantVariableName?: string): boolean => {
|
||||||
if (!assistantVariableName) return false
|
if (!assistantVariableName) return false
|
||||||
const nextBlock = getNextBlock(typebot)(blockId)
|
const nextBlock = getNextBlock(typebot)(blockId)
|
||||||
@@ -131,7 +140,7 @@ const isNextBubbleMessageWithAssistantMessage =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getNextBlock =
|
const getNextBlock =
|
||||||
(typebot: SessionState['typebot']) =>
|
(typebot: TypebotInSession) =>
|
||||||
(blockId: string): Block | undefined => {
|
(blockId: string): Block | undefined => {
|
||||||
const group = typebot.groups.find((group) =>
|
const group = typebot.groups.find((group) =>
|
||||||
group.blocks.find(byId(blockId))
|
group.blocks.find(byId(blockId))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
ChatCompletionOpenAIOptions,
|
ChatCompletionOpenAIOptions,
|
||||||
OpenAICredentials,
|
OpenAICredentials,
|
||||||
} from '@typebot.io/schemas/features/blocks/integrations/openai'
|
} from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||||
import { SessionState } from '@typebot.io/schemas/features/chat'
|
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||||
import { OpenAIStream } from 'ai'
|
import { OpenAIStream } from 'ai'
|
||||||
import {
|
import {
|
||||||
ChatCompletionRequestMessage,
|
ChatCompletionRequestMessage,
|
||||||
@@ -35,7 +35,8 @@ export const getChatCompletionStream =
|
|||||||
credentials.iv
|
credentials.iv
|
||||||
)) as OpenAICredentials['data']
|
)) as OpenAICredentials['data']
|
||||||
|
|
||||||
const temperature = parseVariableNumber(state.typebot.variables)(
|
const { typebot } = state.typebotsQueue[0]
|
||||||
|
const temperature = parseVariableNumber(typebot.variables)(
|
||||||
options.advancedSettings?.temperature
|
options.advancedSettings?.temperature
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ export const resumeChatCompletion =
|
|||||||
const newVariables = options.responseMapping.reduce<
|
const newVariables = options.responseMapping.reduce<
|
||||||
VariableWithUnknowValue[]
|
VariableWithUnknowValue[]
|
||||||
>((newVariables, mapping) => {
|
>((newVariables, mapping) => {
|
||||||
const existingVariable = newSessionState.typebot.variables.find(
|
const { typebot } = newSessionState.typebotsQueue[0]
|
||||||
byId(mapping.variableId)
|
const existingVariable = typebot.variables.find(byId(mapping.variableId))
|
||||||
)
|
|
||||||
if (!existingVariable) return newVariables
|
if (!existingVariable) return newVariables
|
||||||
if (mapping.valueToExtract === 'Message content') {
|
if (mapping.valueToExtract === 'Message content') {
|
||||||
newVariables.push({
|
newVariables.push({
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
|||||||
import { PixelBlock, SessionState } from '@typebot.io/schemas'
|
import { PixelBlock, SessionState } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const executePixelBlock = (
|
export const executePixelBlock = (
|
||||||
{ typebot: { variables }, result }: SessionState,
|
state: SessionState,
|
||||||
block: PixelBlock
|
block: PixelBlock
|
||||||
): ExecuteIntegrationResponse => {
|
): ExecuteIntegrationResponse => {
|
||||||
if (!result || !block.options.pixelId || !block.options.eventType)
|
const { typebot, resultId } = state.typebotsQueue[0]
|
||||||
|
if (!resultId || !block.options.pixelId || !block.options.eventType)
|
||||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
const pixel = deepParseVariables(variables, {
|
const pixel = deepParseVariables(typebot.variables, {
|
||||||
guessCorrectTypes: true,
|
guessCorrectTypes: true,
|
||||||
removeEmptyStrings: true,
|
removeEmptyStrings: true,
|
||||||
})(block.options)
|
})(block.options)
|
||||||
|
|||||||
@@ -3,32 +3,32 @@ import prisma from '@/lib/prisma'
|
|||||||
import { render } from '@faire/mjml-react/utils/render'
|
import { render } from '@faire/mjml-react/utils/render'
|
||||||
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
||||||
import {
|
import {
|
||||||
PublicTypebot,
|
AnswerInSessionState,
|
||||||
ReplyLog,
|
ReplyLog,
|
||||||
ResultInSession,
|
|
||||||
SendEmailBlock,
|
SendEmailBlock,
|
||||||
SendEmailOptions,
|
SendEmailOptions,
|
||||||
SessionState,
|
SessionState,
|
||||||
SmtpCredentials,
|
SmtpCredentials,
|
||||||
|
TypebotInSession,
|
||||||
Variable,
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { createTransport } from 'nodemailer'
|
import { createTransport } from 'nodemailer'
|
||||||
import Mail from 'nodemailer/lib/mailer'
|
import Mail from 'nodemailer/lib/mailer'
|
||||||
import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
||||||
import { parseAnswers } from '@typebot.io/lib/results'
|
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||||
import { decrypt } from '@typebot.io/lib/api'
|
import { decrypt } from '@typebot.io/lib/api'
|
||||||
import { defaultFrom, defaultTransportOptions } from './constants'
|
import { defaultFrom, defaultTransportOptions } from './constants'
|
||||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||||
|
|
||||||
export const executeSendEmailBlock = async (
|
export const executeSendEmailBlock = async (
|
||||||
{ result, typebot }: SessionState,
|
state: SessionState,
|
||||||
block: SendEmailBlock
|
block: SendEmailBlock
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
const { options } = block
|
const { options } = block
|
||||||
const { variables } = typebot
|
const { typebot, resultId, answers } = state.typebotsQueue[0]
|
||||||
const isPreview = !result.id
|
const isPreview = !resultId
|
||||||
if (isPreview)
|
if (isPreview)
|
||||||
return {
|
return {
|
||||||
outgoingEdgeId: block.outgoingEdgeId,
|
outgoingEdgeId: block.outgoingEdgeId,
|
||||||
@@ -41,23 +41,23 @@ export const executeSendEmailBlock = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body =
|
const body =
|
||||||
findUniqueVariableValue(variables)(options.body)?.toString() ??
|
findUniqueVariableValue(typebot.variables)(options.body)?.toString() ??
|
||||||
parseVariables(variables, { escapeHtml: true })(options.body ?? '')
|
parseVariables(typebot.variables, { escapeHtml: true })(options.body ?? '')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sendEmailLogs = await sendEmail({
|
const sendEmailLogs = await sendEmail({
|
||||||
typebotId: typebot.id,
|
typebot,
|
||||||
result,
|
answers,
|
||||||
credentialsId: options.credentialsId,
|
credentialsId: options.credentialsId,
|
||||||
recipients: options.recipients.map(parseVariables(variables)),
|
recipients: options.recipients.map(parseVariables(typebot.variables)),
|
||||||
subject: parseVariables(variables)(options.subject ?? ''),
|
subject: parseVariables(typebot.variables)(options.subject ?? ''),
|
||||||
body,
|
body,
|
||||||
cc: (options.cc ?? []).map(parseVariables(variables)),
|
cc: (options.cc ?? []).map(parseVariables(typebot.variables)),
|
||||||
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
bcc: (options.bcc ?? []).map(parseVariables(typebot.variables)),
|
||||||
replyTo: options.replyTo
|
replyTo: options.replyTo
|
||||||
? parseVariables(variables)(options.replyTo)
|
? parseVariables(typebot.variables)(options.replyTo)
|
||||||
: undefined,
|
: undefined,
|
||||||
fileUrls: getFileUrls(variables)(options.attachmentsVariableId),
|
fileUrls: getFileUrls(typebot.variables)(options.attachmentsVariableId),
|
||||||
isCustomBody: options.isCustomBody,
|
isCustomBody: options.isCustomBody,
|
||||||
isBodyCode: options.isBodyCode,
|
isBodyCode: options.isBodyCode,
|
||||||
})
|
})
|
||||||
@@ -74,8 +74,8 @@ export const executeSendEmailBlock = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendEmail = async ({
|
const sendEmail = async ({
|
||||||
typebotId,
|
typebot,
|
||||||
result,
|
answers,
|
||||||
credentialsId,
|
credentialsId,
|
||||||
recipients,
|
recipients,
|
||||||
body,
|
body,
|
||||||
@@ -87,8 +87,8 @@ const sendEmail = async ({
|
|||||||
isCustomBody,
|
isCustomBody,
|
||||||
fileUrls,
|
fileUrls,
|
||||||
}: SendEmailOptions & {
|
}: SendEmailOptions & {
|
||||||
typebotId: string
|
typebot: TypebotInSession
|
||||||
result: ResultInSession
|
answers: AnswerInSessionState[]
|
||||||
fileUrls?: string | string[]
|
fileUrls?: string | string[]
|
||||||
}): Promise<ReplyLog[] | undefined> => {
|
}): Promise<ReplyLog[] | undefined> => {
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
@@ -112,8 +112,8 @@ const sendEmail = async ({
|
|||||||
body,
|
body,
|
||||||
isCustomBody,
|
isCustomBody,
|
||||||
isBodyCode,
|
isBodyCode,
|
||||||
typebotId,
|
typebot,
|
||||||
result,
|
answersInSession: answers,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!emailBody) {
|
if (!emailBody) {
|
||||||
@@ -206,11 +206,11 @@ const getEmailBody = async ({
|
|||||||
body,
|
body,
|
||||||
isCustomBody,
|
isCustomBody,
|
||||||
isBodyCode,
|
isBodyCode,
|
||||||
typebotId,
|
typebot,
|
||||||
result,
|
answersInSession,
|
||||||
}: {
|
}: {
|
||||||
typebotId: string
|
typebot: TypebotInSession
|
||||||
result: ResultInSession
|
answersInSession: AnswerInSessionState[]
|
||||||
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
|
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
|
||||||
{ html?: string; text?: string } | undefined
|
{ html?: string; text?: string } | undefined
|
||||||
> => {
|
> => {
|
||||||
@@ -219,11 +219,10 @@ const getEmailBody = async ({
|
|||||||
html: isBodyCode ? body : undefined,
|
html: isBodyCode ? body : undefined,
|
||||||
text: !isBodyCode ? body : undefined,
|
text: !isBodyCode ? body : undefined,
|
||||||
}
|
}
|
||||||
const typebot = (await prisma.publicTypebot.findUnique({
|
const answers = parseAnswers({
|
||||||
where: { typebotId },
|
variables: getDefinedVariables(typebot.variables),
|
||||||
})) as unknown as PublicTypebot
|
answers: answersInSession,
|
||||||
if (!typebot) return
|
})
|
||||||
const answers = parseAnswers(typebot, [])(result)
|
|
||||||
return {
|
return {
|
||||||
html: render(
|
html: render(
|
||||||
<DefaultBotNotificationEmail
|
<DefaultBotNotificationEmail
|
||||||
|
|||||||
@@ -6,22 +6,19 @@ import {
|
|||||||
PabblyConnectBlock,
|
PabblyConnectBlock,
|
||||||
SessionState,
|
SessionState,
|
||||||
Webhook,
|
Webhook,
|
||||||
Typebot,
|
|
||||||
Variable,
|
Variable,
|
||||||
WebhookResponse,
|
WebhookResponse,
|
||||||
WebhookOptions,
|
WebhookOptions,
|
||||||
defaultWebhookAttributes,
|
defaultWebhookAttributes,
|
||||||
PublicTypebot,
|
|
||||||
KeyValue,
|
KeyValue,
|
||||||
ReplyLog,
|
ReplyLog,
|
||||||
ResultInSession,
|
|
||||||
ExecutableWebhook,
|
ExecutableWebhook,
|
||||||
|
AnswerInSessionState,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { omit } from '@typebot.io/lib'
|
import { omit } from '@typebot.io/lib'
|
||||||
import { parseAnswers } from '@typebot.io/lib/results'
|
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||||
import got, { Method, HTTPError, OptionsInit } from 'got'
|
import got, { Method, HTTPError, OptionsInit } from 'got'
|
||||||
import { parseSampleResult } from './parseSampleResult'
|
|
||||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||||
import { parseVariables } from '@/features/variables/parseVariables'
|
import { parseVariables } from '@/features/variables/parseVariables'
|
||||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||||
@@ -36,7 +33,6 @@ export const executeWebhookBlock = async (
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
const { typebot, result } = state
|
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
const webhook =
|
const webhook =
|
||||||
block.options.webhook ??
|
block.options.webhook ??
|
||||||
@@ -52,9 +48,8 @@ export const executeWebhookBlock = async (
|
|||||||
}
|
}
|
||||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||||
const parsedWebhook = await parseWebhookAttributes(
|
const parsedWebhook = await parseWebhookAttributes(
|
||||||
typebot,
|
state,
|
||||||
block.groupId,
|
state.typebotsQueue[0].answers
|
||||||
result
|
|
||||||
)(preparedWebhook)
|
)(preparedWebhook)
|
||||||
if (!parsedWebhook) {
|
if (!parsedWebhook) {
|
||||||
logs.push({
|
logs.push({
|
||||||
@@ -97,14 +92,10 @@ const prepareWebhookAttributes = (
|
|||||||
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
||||||
|
|
||||||
const parseWebhookAttributes =
|
const parseWebhookAttributes =
|
||||||
(
|
(state: SessionState, answers: AnswerInSessionState[]) =>
|
||||||
typebot: SessionState['typebot'],
|
|
||||||
groupId: string,
|
|
||||||
result: ResultInSession
|
|
||||||
) =>
|
|
||||||
async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
|
async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
|
||||||
if (!webhook.url || !webhook.method) return
|
if (!webhook.url || !webhook.method) return
|
||||||
const { variables } = typebot
|
const { typebot } = state.typebotsQueue[0]
|
||||||
const basicAuth: { username?: string; password?: string } = {}
|
const basicAuth: { username?: string; password?: string } = {}
|
||||||
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
||||||
(h) =>
|
(h) =>
|
||||||
@@ -121,32 +112,29 @@ const parseWebhookAttributes =
|
|||||||
basicAuth.password = password
|
basicAuth.password = password
|
||||||
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
||||||
}
|
}
|
||||||
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
const headers = convertKeyValueTableToObject(
|
||||||
| ExecutableWebhook['headers']
|
webhook.headers,
|
||||||
| undefined
|
typebot.variables
|
||||||
|
) as ExecutableWebhook['headers'] | undefined
|
||||||
const queryParams = stringify(
|
const queryParams = stringify(
|
||||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
|
||||||
)
|
)
|
||||||
const bodyContent = await getBodyContent(
|
const bodyContent = await getBodyContent({
|
||||||
typebot,
|
|
||||||
[]
|
|
||||||
)({
|
|
||||||
body: webhook.body,
|
body: webhook.body,
|
||||||
result,
|
answers,
|
||||||
groupId,
|
variables: typebot.variables,
|
||||||
variables,
|
|
||||||
})
|
})
|
||||||
const { data: body, isJson } =
|
const { data: body, isJson } =
|
||||||
bodyContent && webhook.method !== HttpMethod.GET
|
bodyContent && webhook.method !== HttpMethod.GET
|
||||||
? safeJsonParse(
|
? safeJsonParse(
|
||||||
parseVariables(variables, {
|
parseVariables(typebot.variables, {
|
||||||
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
||||||
})(bodyContent)
|
})(bodyContent)
|
||||||
)
|
)
|
||||||
: { data: undefined, isJson: false }
|
: { data: undefined, isJson: false }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: parseVariables(variables)(
|
url: parseVariables(typebot.variables)(
|
||||||
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
||||||
),
|
),
|
||||||
basicAuth,
|
basicAuth,
|
||||||
@@ -229,34 +217,25 @@ export const executeWebhook = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBodyContent =
|
const getBodyContent = async ({
|
||||||
(
|
body,
|
||||||
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
answers,
|
||||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
variables,
|
||||||
) =>
|
}: {
|
||||||
async ({
|
body?: string | null
|
||||||
body,
|
answers: AnswerInSessionState[]
|
||||||
result,
|
variables: Variable[]
|
||||||
groupId,
|
}): Promise<string | undefined> => {
|
||||||
variables,
|
if (!body) return
|
||||||
}: {
|
return body === '{{state}}'
|
||||||
body?: string | null
|
? JSON.stringify(
|
||||||
result?: ResultInSession
|
parseAnswers({
|
||||||
groupId: string
|
answers,
|
||||||
variables: Variable[]
|
variables: getDefinedVariables(variables),
|
||||||
}): Promise<string | undefined> => {
|
})
|
||||||
if (!body) return
|
)
|
||||||
return body === '{{state}}'
|
: body
|
||||||
? JSON.stringify(
|
}
|
||||||
result
|
|
||||||
? parseAnswers(typebot, linkedTypebots)(result)
|
|
||||||
: await parseSampleResult(typebot, linkedTypebots)(
|
|
||||||
groupId,
|
|
||||||
variables
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: body
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertKeyValueTableToObject = (
|
const convertKeyValueTableToObject = (
|
||||||
keyValues: KeyValue[] | undefined,
|
keyValues: KeyValue[] | undefined,
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { byId } from '@typebot.io/lib'
|
|||||||
import {
|
import {
|
||||||
MakeComBlock,
|
MakeComBlock,
|
||||||
PabblyConnectBlock,
|
PabblyConnectBlock,
|
||||||
|
ReplyLog,
|
||||||
VariableWithUnknowValue,
|
VariableWithUnknowValue,
|
||||||
WebhookBlock,
|
WebhookBlock,
|
||||||
ZapierBlock,
|
ZapierBlock,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
|
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: SessionState
|
state: SessionState
|
||||||
@@ -27,7 +28,7 @@ export const resumeWebhookExecution = ({
|
|||||||
logs = [],
|
logs = [],
|
||||||
response,
|
response,
|
||||||
}: Props): ExecuteIntegrationResponse => {
|
}: Props): ExecuteIntegrationResponse => {
|
||||||
const { typebot } = state
|
const { typebot } = state.typebotsQueue[0]
|
||||||
const status = response.statusCode.toString()
|
const status = response.statusCode.toString()
|
||||||
const isError = status.startsWith('4') || status.startsWith('5')
|
const isError = status.startsWith('4') || status.startsWith('5')
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
|
|||||||
import { executeCondition } from './executeCondition'
|
import { executeCondition } from './executeCondition'
|
||||||
|
|
||||||
export const executeConditionBlock = (
|
export const executeConditionBlock = (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
block: ConditionBlock
|
block: ConditionBlock
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
const passedCondition = block.items.find((item) =>
|
const passedCondition = block.items.find((item) =>
|
||||||
executeCondition(variables)(item.content)
|
executeCondition(variables)(item.content)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ export const executeJumpBlock = (
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
{ groupId, blockId }: JumpBlock['options']
|
{ groupId, blockId }: JumpBlock['options']
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
const groupToJumpTo = state.typebot.groups.find(
|
const { typebot } = state.typebotsQueue[0]
|
||||||
(group) => group.id === groupId
|
const groupToJumpTo = typebot.groups.find((group) => group.id === groupId)
|
||||||
)
|
|
||||||
const blockToJumpTo =
|
const blockToJumpTo =
|
||||||
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
|
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
|
||||||
groupToJumpTo?.blocks[0]
|
groupToJumpTo?.blocks[0]
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { sanitizeUrl } from '@typebot.io/lib'
|
|||||||
import { ExecuteLogicResponse } from '@/features/chat/types'
|
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||||
|
|
||||||
export const executeRedirect = (
|
export const executeRedirect = (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
block: RedirectBlock
|
block: RedirectBlock
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
|
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
|
|||||||
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
|
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const executeScript = (
|
export const executeScript = (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
block: ScriptBlock
|
block: ScriptBlock
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
|
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
|
|
||||||
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ export const executeSetVariable = (
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
block: SetVariableBlock
|
block: SetVariableBlock
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
const { variables } = state.typebot
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (!block.options?.variableId)
|
if (!block.options?.variableId)
|
||||||
return {
|
return {
|
||||||
outgoingEdgeId: block.outgoingEdgeId,
|
outgoingEdgeId: block.outgoingEdgeId,
|
||||||
}
|
}
|
||||||
const expressionToEvaluate = getExpressionToEvaluate(state.result.id)(
|
const expressionToEvaluate = getExpressionToEvaluate(
|
||||||
block.options
|
state.typebotsQueue[0].resultId
|
||||||
)
|
)(block.options)
|
||||||
const isCustomValue = !block.options.type || block.options.type === 'Custom'
|
const isCustomValue = !block.options.type || block.options.type === 'Custom'
|
||||||
if (
|
if (
|
||||||
expressionToEvaluate &&
|
expressionToEvaluate &&
|
||||||
@@ -25,7 +25,7 @@ export const executeSetVariable = (
|
|||||||
block.options.type === 'Moment of the day')
|
block.options.type === 'Moment of the day')
|
||||||
) {
|
) {
|
||||||
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
||||||
state.typebot.variables,
|
variables,
|
||||||
expressionToEvaluate
|
expressionToEvaluate
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,19 +6,24 @@ import prisma from '@/lib/prisma'
|
|||||||
import {
|
import {
|
||||||
TypebotLinkBlock,
|
TypebotLinkBlock,
|
||||||
SessionState,
|
SessionState,
|
||||||
TypebotInSession,
|
|
||||||
Variable,
|
Variable,
|
||||||
ReplyLog,
|
ReplyLog,
|
||||||
|
Typebot,
|
||||||
|
VariableWithValue,
|
||||||
|
Edge,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { byId } from '@typebot.io/lib'
|
|
||||||
import { ExecuteLogicResponse } from '@/features/chat/types'
|
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
|
||||||
|
import { createResultIfNotExist } from '@/features/chat/queries/createResultIfNotExist'
|
||||||
|
|
||||||
export const executeTypebotLink = async (
|
export const executeTypebotLink = async (
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
block: TypebotLinkBlock
|
block: TypebotLinkBlock
|
||||||
): Promise<ExecuteLogicResponse> => {
|
): Promise<ExecuteLogicResponse> => {
|
||||||
const logs: ReplyLog[] = []
|
const logs: ReplyLog[] = []
|
||||||
if (!block.options.typebotId) {
|
const typebotId = block.options.typebotId
|
||||||
|
if (!typebotId) {
|
||||||
logs.push({
|
logs.push({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
description: `Failed to link typebot`,
|
description: `Failed to link typebot`,
|
||||||
@@ -26,7 +31,7 @@ export const executeTypebotLink = async (
|
|||||||
})
|
})
|
||||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||||
}
|
}
|
||||||
const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId)
|
const linkedTypebot = await fetchTypebot(state, typebotId)
|
||||||
if (!linkedTypebot) {
|
if (!linkedTypebot) {
|
||||||
logs.push({
|
logs.push({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -35,12 +40,17 @@ export const executeTypebotLink = async (
|
|||||||
})
|
})
|
||||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||||
}
|
}
|
||||||
let newSessionState = addLinkedTypebotToState(state, block, linkedTypebot)
|
let newSessionState = await addLinkedTypebotToState(
|
||||||
|
state,
|
||||||
|
block,
|
||||||
|
linkedTypebot
|
||||||
|
)
|
||||||
|
|
||||||
const nextGroupId =
|
const nextGroupId =
|
||||||
block.options.groupId ??
|
block.options.groupId ??
|
||||||
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
linkedTypebot.groups.find((group) =>
|
||||||
?.id
|
group.blocks.some((block) => block.type === 'start')
|
||||||
|
)?.id
|
||||||
if (!nextGroupId) {
|
if (!nextGroupId) {
|
||||||
logs.push({
|
logs.push({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -60,76 +70,123 @@ export const executeTypebotLink = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addLinkedTypebotToState = (
|
const addLinkedTypebotToState = async (
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
block: TypebotLinkBlock,
|
block: TypebotLinkBlock,
|
||||||
linkedTypebot: TypebotInSession
|
linkedTypebot: Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
|
||||||
): SessionState => {
|
): Promise<SessionState> => {
|
||||||
const incomingVariables = fillVariablesWithExistingValues(
|
const currentTypebotInQueue = state.typebotsQueue[0]
|
||||||
linkedTypebot.variables,
|
const isPreview = isNotDefined(currentTypebotInQueue.resultId)
|
||||||
state.typebot.variables
|
|
||||||
)
|
const resumeEdge = createResumeEdgeIfNecessary(state, block)
|
||||||
|
|
||||||
|
const currentTypebotWithResumeEdge = resumeEdge
|
||||||
|
? {
|
||||||
|
...currentTypebotInQueue,
|
||||||
|
typebot: {
|
||||||
|
...currentTypebotInQueue.typebot,
|
||||||
|
edges: [...currentTypebotInQueue.typebot.edges, resumeEdge],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: currentTypebotInQueue
|
||||||
|
|
||||||
|
const shouldMergeResults =
|
||||||
|
block.options.mergeResults !== false ||
|
||||||
|
currentTypebotInQueue.typebot.id === linkedTypebot.id ||
|
||||||
|
block.options.typebotId === 'current'
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentTypebotInQueue.resultId &&
|
||||||
|
currentTypebotInQueue.answers.length === 0 &&
|
||||||
|
shouldMergeResults
|
||||||
|
) {
|
||||||
|
await createResultIfNotExist({
|
||||||
|
resultId: currentTypebotInQueue.resultId,
|
||||||
|
typebot: currentTypebotInQueue.typebot,
|
||||||
|
hasStarted: false,
|
||||||
|
isCompleted: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
typebot: {
|
typebotsQueue: [
|
||||||
...state.typebot,
|
{
|
||||||
groups: [...state.typebot.groups, ...linkedTypebot.groups],
|
typebot: {
|
||||||
variables: [...state.typebot.variables, ...incomingVariables],
|
...linkedTypebot,
|
||||||
edges: [...state.typebot.edges, ...linkedTypebot.edges],
|
variables: fillVariablesWithExistingValues(
|
||||||
|
linkedTypebot.variables,
|
||||||
|
currentTypebotInQueue.typebot.variables
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resultId: isPreview
|
||||||
|
? undefined
|
||||||
|
: shouldMergeResults
|
||||||
|
? currentTypebotInQueue.resultId
|
||||||
|
: createId(),
|
||||||
|
edgeIdToTriggerWhenDone: block.outgoingEdgeId ?? resumeEdge?.id,
|
||||||
|
answers: shouldMergeResults ? currentTypebotInQueue.answers : [],
|
||||||
|
isMergingWithParent: shouldMergeResults,
|
||||||
|
},
|
||||||
|
currentTypebotWithResumeEdge,
|
||||||
|
...state.typebotsQueue.slice(1),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResumeEdgeIfNecessary = (
|
||||||
|
state: SessionState,
|
||||||
|
block: TypebotLinkBlock
|
||||||
|
): Edge | undefined => {
|
||||||
|
const currentTypebotInQueue = state.typebotsQueue[0]
|
||||||
|
const blockId = block.id
|
||||||
|
if (block.outgoingEdgeId) return
|
||||||
|
const currentGroup = currentTypebotInQueue.typebot.groups.find((group) =>
|
||||||
|
group.blocks.some((block) => block.id === blockId)
|
||||||
|
)
|
||||||
|
if (!currentGroup) return
|
||||||
|
const currentBlockIndex = currentGroup.blocks.findIndex(
|
||||||
|
(block) => block.id === blockId
|
||||||
|
)
|
||||||
|
const nextBlockInGroup =
|
||||||
|
currentBlockIndex === -1
|
||||||
|
? undefined
|
||||||
|
: currentGroup.blocks[currentBlockIndex + 1]
|
||||||
|
if (!nextBlockInGroup) return
|
||||||
|
return {
|
||||||
|
id: createId(),
|
||||||
|
from: {
|
||||||
|
groupId: '',
|
||||||
|
blockId: '',
|
||||||
},
|
},
|
||||||
linkedTypebots: {
|
to: {
|
||||||
typebots: [
|
groupId: nextBlockInGroup.groupId,
|
||||||
...state.linkedTypebots.typebots.filter(
|
blockId: nextBlockInGroup.id,
|
||||||
(existingTypebots) => existingTypebots.id !== linkedTypebot.id
|
|
||||||
),
|
|
||||||
],
|
|
||||||
queue: block.outgoingEdgeId
|
|
||||||
? [
|
|
||||||
...state.linkedTypebots.queue,
|
|
||||||
{ edgeId: block.outgoingEdgeId, typebotId: state.currentTypebotId },
|
|
||||||
]
|
|
||||||
: state.linkedTypebots.queue,
|
|
||||||
},
|
},
|
||||||
currentTypebotId: linkedTypebot.id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fillVariablesWithExistingValues = (
|
const fillVariablesWithExistingValues = (
|
||||||
variables: Variable[],
|
variables: Variable[],
|
||||||
variablesWithValues: Variable[]
|
variablesWithValues: Variable[]
|
||||||
): Variable[] =>
|
): VariableWithValue[] =>
|
||||||
variables.map((variable) => {
|
variables
|
||||||
const matchedVariable = variablesWithValues.find(
|
.map((variable) => {
|
||||||
(variableWithValue) => variableWithValue.name === variable.name
|
const matchedVariable = variablesWithValues.find(
|
||||||
)
|
(variableWithValue) => variableWithValue.name === variable.name
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...variable,
|
...variable,
|
||||||
value: matchedVariable?.value ?? variable.value,
|
value: matchedVariable?.value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter((variable) => isDefined(variable.value)) as VariableWithValue[]
|
||||||
|
|
||||||
const getLinkedTypebot = async (
|
const fetchTypebot = async (state: SessionState, typebotId: string) => {
|
||||||
state: SessionState,
|
const { typebot: typebotInState, resultId } = state.typebotsQueue[0]
|
||||||
typebotId: string
|
const isPreview = !resultId
|
||||||
): Promise<TypebotInSession | null> => {
|
if (typebotId === 'current') return typebotInState
|
||||||
const { typebot, result } = state
|
|
||||||
const isPreview = !result.id
|
|
||||||
if (typebotId === 'current') return typebot
|
|
||||||
const availableTypebots =
|
|
||||||
'linkedTypebots' in state
|
|
||||||
? [typebot, ...state.linkedTypebots.typebots]
|
|
||||||
: [typebot]
|
|
||||||
const linkedTypebot =
|
|
||||||
availableTypebots.find(byId(typebotId)) ??
|
|
||||||
(await fetchTypebot(isPreview, typebotId))
|
|
||||||
return linkedTypebot
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchTypebot = async (
|
|
||||||
isPreview: boolean,
|
|
||||||
typebotId: string
|
|
||||||
): Promise<TypebotInSession | null> => {
|
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
const typebot = await prisma.typebot.findUnique({
|
const typebot = await prisma.typebot.findUnique({
|
||||||
where: { id: typebotId },
|
where: { id: typebotId },
|
||||||
@@ -140,7 +197,7 @@ const fetchTypebot = async (
|
|||||||
variables: true,
|
variables: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return typebot as TypebotInSession
|
return typebot as Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
|
||||||
}
|
}
|
||||||
const typebot = await prisma.publicTypebot.findUnique({
|
const typebot = await prisma.publicTypebot.findUnique({
|
||||||
where: { typebotId },
|
where: { typebotId },
|
||||||
@@ -155,5 +212,5 @@ const fetchTypebot = async (
|
|||||||
return {
|
return {
|
||||||
...typebot,
|
...typebot,
|
||||||
id: typebotId,
|
id: typebotId,
|
||||||
} as TypebotInSession
|
} as Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import test, { expect } from '@playwright/test'
|
|||||||
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
||||||
|
|
||||||
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
|
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
|
||||||
|
const typebotWithMergeDisabledId = 'cl0ibhi7s0018n21aarlag0cm'
|
||||||
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
|
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
@@ -11,6 +12,13 @@ test.beforeAll(async () => {
|
|||||||
getTestAsset('typebots/linkTypebots/1.json'),
|
getTestAsset('typebots/linkTypebots/1.json'),
|
||||||
{ id: typebotId, publicId: `${typebotId}-public` }
|
{ id: typebotId, publicId: `${typebotId}-public` }
|
||||||
)
|
)
|
||||||
|
await importTypebotInDatabase(
|
||||||
|
getTestAsset('typebots/linkTypebots/1-merge-disabled.json'),
|
||||||
|
{
|
||||||
|
id: typebotWithMergeDisabledId,
|
||||||
|
publicId: `${typebotWithMergeDisabledId}-public`,
|
||||||
|
}
|
||||||
|
)
|
||||||
await importTypebotInDatabase(
|
await importTypebotInDatabase(
|
||||||
getTestAsset('typebots/linkTypebots/2.json'),
|
getTestAsset('typebots/linkTypebots/2.json'),
|
||||||
{ id: linkedTypebotId, publicId: `${linkedTypebotId}-public` }
|
{ id: linkedTypebotId, publicId: `${linkedTypebotId}-public` }
|
||||||
@@ -28,3 +36,21 @@ test('should work as expected', async ({ page }) => {
|
|||||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||||
await expect(page.locator('text=Hello there!')).toBeVisible()
|
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Merge disabled', () => {
|
||||||
|
test('should work as expected', async ({ page }) => {
|
||||||
|
await page.goto(`/${typebotWithMergeDisabledId}-public`)
|
||||||
|
await page.locator('input').fill('Hello there!')
|
||||||
|
await page.locator('input').press('Enter')
|
||||||
|
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||||
|
await page.goto(
|
||||||
|
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`
|
||||||
|
)
|
||||||
|
await expect(page.locator('text=Submitted at')).toBeVisible()
|
||||||
|
await expect(page.locator('text=Hello there!')).toBeHidden()
|
||||||
|
await page.goto(
|
||||||
|
`${process.env.NEXTAUTH_URL}/typebots/${linkedTypebotId}/results`
|
||||||
|
)
|
||||||
|
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
|
|||||||
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const executeWait = (
|
export const executeWait = (
|
||||||
{ typebot: { variables } }: SessionState,
|
state: SessionState,
|
||||||
block: WaitBlock
|
block: WaitBlock
|
||||||
): ExecuteLogicResponse => {
|
): ExecuteLogicResponse => {
|
||||||
|
const { variables } = state.typebotsQueue[0].typebot
|
||||||
if (!block.options.secondsToWaitFor)
|
if (!block.options.secondsToWaitFor)
|
||||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
const parsedSecondsToWaitFor = safeParseInt(
|
const parsedSecondsToWaitFor = safeParseInt(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
IntegrationBlockType,
|
IntegrationBlockType,
|
||||||
PixelBlock,
|
PixelBlock,
|
||||||
ReplyLog,
|
ReplyLog,
|
||||||
ResultInSession,
|
|
||||||
sendMessageInputSchema,
|
sendMessageInputSchema,
|
||||||
SessionState,
|
SessionState,
|
||||||
StartParams,
|
StartParams,
|
||||||
@@ -148,22 +147,19 @@ const startSession = async (
|
|||||||
: prefilledVariables
|
: prefilledVariables
|
||||||
|
|
||||||
const initialState: SessionState = {
|
const initialState: SessionState = {
|
||||||
typebot: {
|
version: '2',
|
||||||
id: typebot.id,
|
typebotsQueue: [
|
||||||
groups: typebot.groups,
|
{
|
||||||
edges: typebot.edges,
|
resultId: result?.id,
|
||||||
variables: startVariables,
|
typebot: {
|
||||||
},
|
id: typebot.id,
|
||||||
linkedTypebots: {
|
groups: typebot.groups,
|
||||||
typebots: [],
|
edges: typebot.edges,
|
||||||
queue: [],
|
variables: startVariables,
|
||||||
},
|
},
|
||||||
result: {
|
answers: [],
|
||||||
id: result?.id,
|
},
|
||||||
variables: result?.variables ?? [],
|
],
|
||||||
answers: result?.answers ?? [],
|
|
||||||
},
|
|
||||||
currentTypebotId: typebot.id,
|
|
||||||
dynamicTheme: parseDynamicThemeInState(typebot.theme),
|
dynamicTheme: parseDynamicThemeInState(typebot.theme),
|
||||||
isStreamEnabled: startParams.isStreamEnabled,
|
isStreamEnabled: startParams.isStreamEnabled,
|
||||||
}
|
}
|
||||||
@@ -212,12 +208,12 @@ const startSession = async (
|
|||||||
startClientSideAction.length > 0 ? startClientSideAction : undefined,
|
startClientSideAction.length > 0 ? startClientSideAction : undefined,
|
||||||
typebot: {
|
typebot: {
|
||||||
id: typebot.id,
|
id: typebot.id,
|
||||||
settings: deepParseVariables(newSessionState.typebot.variables)(
|
settings: deepParseVariables(
|
||||||
typebot.settings
|
newSessionState.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(typebot.settings),
|
||||||
theme: deepParseVariables(newSessionState.typebot.variables)(
|
theme: deepParseVariables(
|
||||||
typebot.theme
|
newSessionState.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(typebot.theme),
|
||||||
},
|
},
|
||||||
dynamicTheme: parseDynamicThemeReply(newSessionState),
|
dynamicTheme: parseDynamicThemeReply(newSessionState),
|
||||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||||
@@ -239,12 +235,12 @@ const startSession = async (
|
|||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
typebot: {
|
typebot: {
|
||||||
id: typebot.id,
|
id: typebot.id,
|
||||||
settings: deepParseVariables(newSessionState.typebot.variables)(
|
settings: deepParseVariables(
|
||||||
typebot.settings
|
newSessionState.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(typebot.settings),
|
||||||
theme: deepParseVariables(newSessionState.typebot.variables)(
|
theme: deepParseVariables(
|
||||||
typebot.theme
|
newSessionState.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(typebot.theme),
|
||||||
},
|
},
|
||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
@@ -319,7 +315,7 @@ const getResult = async ({
|
|||||||
if (isPreview) return
|
if (isPreview) return
|
||||||
const existingResult =
|
const existingResult =
|
||||||
resultId && isRememberUserEnabled
|
resultId && isRememberUserEnabled
|
||||||
? ((await findResult({ id: resultId })) as ResultInSession)
|
? await findResult({ id: resultId })
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const prefilledVariableWithValue = prefilledVariables.filter(
|
const prefilledVariableWithValue = prefilledVariables.filter(
|
||||||
@@ -341,7 +337,7 @@ const getResult = async ({
|
|||||||
return {
|
return {
|
||||||
id: existingResult?.id ?? createId(),
|
id: existingResult?.id ?? createId(),
|
||||||
variables: updatedResult.variables,
|
variables: updatedResult.variables,
|
||||||
answers: existingResult?.answers,
|
answers: existingResult?.answers ?? [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,10 +365,10 @@ const parseDynamicThemeReply = (
|
|||||||
): ChatReply['dynamicTheme'] => {
|
): ChatReply['dynamicTheme'] => {
|
||||||
if (!state?.dynamicTheme) return
|
if (!state?.dynamicTheme) return
|
||||||
return {
|
return {
|
||||||
hostAvatarUrl: parseVariables(state?.typebot.variables)(
|
hostAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
|
||||||
state.dynamicTheme.hostAvatarUrl
|
state.dynamicTheme.hostAvatarUrl
|
||||||
),
|
),
|
||||||
guestAvatarUrl: parseVariables(state?.typebot.variables)(
|
guestAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
|
||||||
state.dynamicTheme.guestAvatarUrl
|
state.dynamicTheme.guestAvatarUrl
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { TRPCError } from '@trpc/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getSession } from '../queries/getSession'
|
import { getSession } from '../queries/getSession'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { PublicTypebot, SessionState, Typebot } from '@typebot.io/schemas'
|
import {
|
||||||
|
PublicTypebot,
|
||||||
|
SessionState,
|
||||||
|
Typebot,
|
||||||
|
Variable,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const updateTypebotInSession = publicProcedure
|
export const updateTypebotInSession = publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -32,7 +37,7 @@ export const updateTypebotInSession = publicProcedure
|
|||||||
const publicTypebot = (await prisma.publicTypebot.findFirst({
|
const publicTypebot = (await prisma.publicTypebot.findFirst({
|
||||||
where: {
|
where: {
|
||||||
typebot: {
|
typebot: {
|
||||||
id: session.state.typebot.id,
|
id: session.state.typebotsQueue[0].typebot.id,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
@@ -74,21 +79,28 @@ const updateSessionState = (
|
|||||||
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
|
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
|
||||||
): SessionState => ({
|
): SessionState => ({
|
||||||
...currentState,
|
...currentState,
|
||||||
typebot: {
|
typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
|
||||||
...currentState.typebot,
|
index === 0
|
||||||
edges: newTypebot.edges,
|
? {
|
||||||
variables: updateVariablesInSession(
|
...typebotInQueue,
|
||||||
currentState.typebot.variables,
|
typebot: {
|
||||||
newTypebot.variables
|
...typebotInQueue.typebot,
|
||||||
),
|
edges: newTypebot.edges,
|
||||||
groups: newTypebot.groups,
|
groups: newTypebot.groups,
|
||||||
},
|
variables: updateVariablesInSession(
|
||||||
|
typebotInQueue.typebot.variables,
|
||||||
|
newTypebot.variables
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: typebotInQueue
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateVariablesInSession = (
|
const updateVariablesInSession = (
|
||||||
currentVariables: SessionState['typebot']['variables'],
|
currentVariables: Variable[],
|
||||||
newVariables: Typebot['variables']
|
newVariables: Typebot['variables']
|
||||||
): SessionState['typebot']['variables'] => [
|
): Variable[] => [
|
||||||
...currentVariables,
|
...currentVariables,
|
||||||
...newVariables.filter(
|
...newVariables.filter(
|
||||||
(newVariable) =>
|
(newVariable) =>
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ export const addEdgeToTypebot = (
|
|||||||
edge: Edge
|
edge: Edge
|
||||||
): SessionState => ({
|
): SessionState => ({
|
||||||
...state,
|
...state,
|
||||||
typebot: {
|
typebotsQueue: state.typebotsQueue.map((typebot, index) =>
|
||||||
...state.typebot,
|
index === 0
|
||||||
edges: [...state.typebot.edges, edge],
|
? {
|
||||||
},
|
...typebot,
|
||||||
|
typebot: {
|
||||||
|
...typebot.typebot,
|
||||||
|
edges: [...typebot.typebot.edges, edge],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: typebot
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
|
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import {
|
import {
|
||||||
|
AnswerInSessionState,
|
||||||
Block,
|
Block,
|
||||||
BlockType,
|
BlockType,
|
||||||
BubbleBlockType,
|
BubbleBlockType,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
InputBlockType,
|
InputBlockType,
|
||||||
IntegrationBlockType,
|
IntegrationBlockType,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
ResultInSession,
|
|
||||||
SessionState,
|
SessionState,
|
||||||
SetVariableBlock,
|
SetVariableBlock,
|
||||||
WebhookBlock,
|
WebhookBlock,
|
||||||
@@ -35,7 +35,7 @@ export const continueBotFlow =
|
|||||||
reply?: string
|
reply?: string
|
||||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||||
let newSessionState = { ...state }
|
let newSessionState = { ...state }
|
||||||
const group = state.typebot.groups.find(
|
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||||
(group) => group.id === state.currentBlock?.groupId
|
(group) => group.id === state.currentBlock?.groupId
|
||||||
)
|
)
|
||||||
const blockIndex =
|
const blockIndex =
|
||||||
@@ -52,7 +52,7 @@ export const continueBotFlow =
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (block.type === LogicBlockType.SET_VARIABLE) {
|
if (block.type === LogicBlockType.SET_VARIABLE) {
|
||||||
const existingVariable = state.typebot.variables.find(
|
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||||
byId(block.options.variableId)
|
byId(block.options.variableId)
|
||||||
)
|
)
|
||||||
if (existingVariable && reply) {
|
if (existingVariable && reply) {
|
||||||
@@ -103,7 +103,8 @@ export const continueBotFlow =
|
|||||||
formattedReply
|
formattedReply
|
||||||
)
|
)
|
||||||
const itemId = nextEdgeId
|
const itemId = nextEdgeId
|
||||||
? state.typebot.edges.find(byId(nextEdgeId))?.from.itemId
|
? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
|
||||||
|
?.from.itemId
|
||||||
: undefined
|
: undefined
|
||||||
newSessionState = await processAndSaveAnswer(
|
newSessionState = await processAndSaveAnswer(
|
||||||
state,
|
state,
|
||||||
@@ -128,7 +129,7 @@ export const continueBotFlow =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextEdgeId && state.linkedTypebots.queue.length === 0)
|
if (!nextEdgeId && state.typebotsQueue.length === 1)
|
||||||
return {
|
return {
|
||||||
messages: [],
|
messages: [],
|
||||||
newSessionState,
|
newSessionState,
|
||||||
@@ -138,7 +139,9 @@ export const continueBotFlow =
|
|||||||
|
|
||||||
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
|
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
|
||||||
|
|
||||||
if (!nextGroup)
|
newSessionState = nextGroup.newSessionState
|
||||||
|
|
||||||
|
if (!nextGroup.group)
|
||||||
return {
|
return {
|
||||||
messages: [],
|
messages: [],
|
||||||
newSessionState,
|
newSessionState,
|
||||||
@@ -168,7 +171,7 @@ const saveVariableValueIfAny =
|
|||||||
(state: SessionState, block: InputBlock) =>
|
(state: SessionState, block: InputBlock) =>
|
||||||
(reply: string): SessionState => {
|
(reply: string): SessionState => {
|
||||||
if (!block.options.variableId) return state
|
if (!block.options.variableId) return state
|
||||||
const foundVariable = state.typebot.variables.find(
|
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||||
(variable) => variable.id === block.options.variableId
|
(variable) => variable.id === block.options.variableId
|
||||||
)
|
)
|
||||||
if (!foundVariable) return state
|
if (!foundVariable) return state
|
||||||
@@ -235,34 +238,47 @@ const saveAnswer =
|
|||||||
itemId,
|
itemId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const key = block.options.variableId
|
||||||
|
? state.typebotsQueue[0].typebot.variables.find(
|
||||||
|
(variable) => variable.id === block.options.variableId
|
||||||
|
)?.name
|
||||||
|
: state.typebotsQueue[0].typebot.groups.find((group) =>
|
||||||
|
group.blocks.find((blockInGroup) => blockInGroup.id === block.id)
|
||||||
|
)?.title
|
||||||
|
|
||||||
return setNewAnswerInState(state)({
|
return setNewAnswerInState(state)({
|
||||||
blockId: block.id,
|
key: key ?? block.id,
|
||||||
variableId: block.options.variableId ?? null,
|
value: reply,
|
||||||
content: reply,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setNewAnswerInState =
|
const setNewAnswerInState =
|
||||||
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
|
(state: SessionState) => (newAnswer: AnswerInSessionState) => {
|
||||||
const newAnswers = state.result.answers
|
const answers = state.typebotsQueue[0].answers
|
||||||
.filter((answer) => answer.blockId !== newAnswer.blockId)
|
const newAnswers = answers
|
||||||
|
.filter((answer) => answer.key !== newAnswer.key)
|
||||||
.concat(newAnswer)
|
.concat(newAnswer)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
result: {
|
typebotsQueue: state.typebotsQueue.map((typebot, index) =>
|
||||||
...state.result,
|
index === 0
|
||||||
answers: newAnswers,
|
? {
|
||||||
},
|
...typebot,
|
||||||
|
answers: newAnswers,
|
||||||
|
}
|
||||||
|
: typebot
|
||||||
|
),
|
||||||
} satisfies SessionState
|
} satisfies SessionState
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOutgoingEdgeId =
|
const getOutgoingEdgeId =
|
||||||
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
|
(state: Pick<SessionState, 'typebotsQueue'>) =>
|
||||||
(
|
(
|
||||||
block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock,
|
block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock,
|
||||||
reply: string | undefined
|
reply: string | undefined
|
||||||
) => {
|
) => {
|
||||||
|
const variables = state.typebotsQueue[0].typebot.variables
|
||||||
if (
|
if (
|
||||||
block.type === InputBlockType.CHOICE &&
|
block.type === InputBlockType.CHOICE &&
|
||||||
!block.options.isMultipleChoice &&
|
!block.options.isMultipleChoice &&
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
InputBlockType,
|
InputBlockType,
|
||||||
RuntimeOptions,
|
RuntimeOptions,
|
||||||
SessionState,
|
SessionState,
|
||||||
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import {
|
import {
|
||||||
isBubbleBlock,
|
isBubbleBlock,
|
||||||
@@ -47,7 +48,9 @@ export const executeGroup =
|
|||||||
|
|
||||||
if (isBubbleBlock(block)) {
|
if (isBubbleBlock(block)) {
|
||||||
messages.push(
|
messages.push(
|
||||||
parseBubbleBlock(newSessionState.typebot.variables)(block)
|
parseBubbleBlock(newSessionState.typebotsQueue[0].typebot.variables)(
|
||||||
|
block
|
||||||
|
)
|
||||||
)
|
)
|
||||||
lastBubbleBlockId = block.id
|
lastBubbleBlockId = block.id
|
||||||
continue
|
continue
|
||||||
@@ -118,14 +121,14 @@ export const executeGroup =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextEdgeId)
|
if (!nextEdgeId && state.typebotsQueue.length === 1)
|
||||||
return { messages, newSessionState, clientSideActions, logs }
|
return { messages, newSessionState, clientSideActions, logs }
|
||||||
|
|
||||||
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
|
const nextGroup = getNextGroup(newSessionState)(nextEdgeId ?? undefined)
|
||||||
|
|
||||||
if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext
|
newSessionState = nextGroup.newSessionState
|
||||||
|
|
||||||
if (!nextGroup) {
|
if (!nextGroup.group) {
|
||||||
return { messages, newSessionState, clientSideActions, logs }
|
return { messages, newSessionState, clientSideActions, logs }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +144,7 @@ export const executeGroup =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const computeRuntimeOptions =
|
const computeRuntimeOptions =
|
||||||
(state: Pick<SessionState, 'result' | 'typebot'>) =>
|
(state: SessionState) =>
|
||||||
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
|
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case InputBlockType.PAYMENT: {
|
case InputBlockType.PAYMENT: {
|
||||||
@@ -151,7 +154,7 @@ const computeRuntimeOptions =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getPrefilledInputValue =
|
const getPrefilledInputValue =
|
||||||
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
|
(variables: Variable[]) => (block: InputBlock) => {
|
||||||
const variableValue = variables.find(
|
const variableValue = variables.find(
|
||||||
(variable) =>
|
(variable) =>
|
||||||
variable.id === block.options.variableId && isDefined(variable.value)
|
variable.id === block.options.variableId && isDefined(variable.value)
|
||||||
@@ -161,7 +164,7 @@ const getPrefilledInputValue =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parseBubbleBlock =
|
const parseBubbleBlock =
|
||||||
(variables: SessionState['typebot']['variables']) =>
|
(variables: Variable[]) =>
|
||||||
(block: BubbleBlock): ChatReply['messages'][0] => {
|
(block: BubbleBlock): ChatReply['messages'][0] => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case BubbleBlockType.TEXT:
|
case BubbleBlockType.TEXT:
|
||||||
@@ -197,15 +200,17 @@ const parseInput =
|
|||||||
}
|
}
|
||||||
case InputBlockType.PICTURE_CHOICE: {
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
return injectVariableValuesInPictureChoiceBlock(
|
return injectVariableValuesInPictureChoiceBlock(
|
||||||
state.typebot.variables
|
state.typebotsQueue[0].typebot.variables
|
||||||
)(block)
|
)(block)
|
||||||
}
|
}
|
||||||
case InputBlockType.NUMBER: {
|
case InputBlockType.NUMBER: {
|
||||||
const parsedBlock = deepParseVariables(state.typebot.variables)({
|
const parsedBlock = deepParseVariables(
|
||||||
|
state.typebotsQueue[0].typebot.variables
|
||||||
|
)({
|
||||||
...block,
|
...block,
|
||||||
prefilledValue: getPrefilledInputValue(state.typebot.variables)(
|
prefilledValue: getPrefilledInputValue(
|
||||||
block
|
state.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(block),
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
...parsedBlock,
|
...parsedBlock,
|
||||||
@@ -224,12 +229,12 @@ const parseInput =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return deepParseVariables(state.typebot.variables)({
|
return deepParseVariables(state.typebotsQueue[0].typebot.variables)({
|
||||||
...block,
|
...block,
|
||||||
runtimeOptions: await computeRuntimeOptions(state)(block),
|
runtimeOptions: await computeRuntimeOptions(state)(block),
|
||||||
prefilledValue: getPrefilledInputValue(state.typebot.variables)(
|
prefilledValue: getPrefilledInputValue(
|
||||||
block
|
state.typebotsQueue[0].typebot.variables
|
||||||
),
|
)(block),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,81 @@ import { byId } from '@typebot.io/lib'
|
|||||||
import { Group, SessionState } from '@typebot.io/schemas'
|
import { Group, SessionState } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export type NextGroup = {
|
export type NextGroup = {
|
||||||
group: Group
|
group?: Group
|
||||||
updatedContext?: SessionState
|
newSessionState: SessionState
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNextGroup =
|
export const getNextGroup =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
(edgeId?: string): NextGroup | null => {
|
(edgeId?: string): NextGroup => {
|
||||||
const { typebot } = state
|
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
|
||||||
const nextEdge = typebot.edges.find(byId(edgeId))
|
|
||||||
if (!nextEdge) {
|
if (!nextEdge) {
|
||||||
if (state.linkedTypebots.queue.length > 0) {
|
if (state.typebotsQueue.length > 1) {
|
||||||
const nextEdgeId = state.linkedTypebots.queue[0].edgeId
|
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
|
||||||
const updatedContext = {
|
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
|
||||||
|
const newSessionState = {
|
||||||
...state,
|
...state,
|
||||||
linkedBotQueue: state.linkedTypebots.queue.slice(1),
|
typebotsQueue: [
|
||||||
}
|
{
|
||||||
const nextGroup = getNextGroup(updatedContext)(nextEdgeId)
|
...state.typebotsQueue[1],
|
||||||
if (!nextGroup) return null
|
typebot: isMergingWithParent
|
||||||
|
? {
|
||||||
|
...state.typebotsQueue[1].typebot,
|
||||||
|
variables: state.typebotsQueue[1].typebot.variables.map(
|
||||||
|
(variable) => ({
|
||||||
|
...variable,
|
||||||
|
value: state.typebotsQueue[0].answers.find(
|
||||||
|
(answer) => answer.key === variable.name
|
||||||
|
)?.value,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: state.typebotsQueue[1].typebot,
|
||||||
|
answers: isMergingWithParent
|
||||||
|
? [
|
||||||
|
...state.typebotsQueue[1].answers.filter(
|
||||||
|
(incomingAnswer) =>
|
||||||
|
!state.typebotsQueue[0].answers.find(
|
||||||
|
(currentAnswer) =>
|
||||||
|
currentAnswer.key === incomingAnswer.key
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...state.typebotsQueue[0].answers,
|
||||||
|
]
|
||||||
|
: state.typebotsQueue[1].answers,
|
||||||
|
},
|
||||||
|
...state.typebotsQueue.slice(2),
|
||||||
|
],
|
||||||
|
} satisfies SessionState
|
||||||
|
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
|
||||||
|
if (!nextGroup)
|
||||||
|
return {
|
||||||
|
newSessionState,
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...nextGroup,
|
...nextGroup,
|
||||||
updatedContext,
|
newSessionState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return {
|
||||||
|
newSessionState: state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const nextGroup = typebot.groups.find(byId(nextEdge.to.groupId))
|
const nextGroup = state.typebotsQueue[0].typebot.groups.find(
|
||||||
if (!nextGroup) return null
|
byId(nextEdge.to.groupId)
|
||||||
|
)
|
||||||
|
if (!nextGroup)
|
||||||
|
return {
|
||||||
|
newSessionState: state,
|
||||||
|
}
|
||||||
const startBlockIndex = nextEdge.to.blockId
|
const startBlockIndex = nextEdge.to.blockId
|
||||||
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
|
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
|
||||||
: 0
|
: 0
|
||||||
return {
|
return {
|
||||||
group: { ...nextGroup, blocks: nextGroup.blocks.slice(startBlockIndex) },
|
group: {
|
||||||
|
...nextGroup,
|
||||||
|
blocks: nextGroup.blocks.slice(startBlockIndex),
|
||||||
|
},
|
||||||
|
newSessionState: state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Props = {
|
|||||||
logs: ChatReply['logs']
|
logs: ChatReply['logs']
|
||||||
clientSideActions: ChatReply['clientSideActions']
|
clientSideActions: ChatReply['clientSideActions']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveStateToDatabase = async ({
|
export const saveStateToDatabase = async ({
|
||||||
session: { state, id },
|
session: { state, id },
|
||||||
input,
|
input,
|
||||||
@@ -21,25 +22,30 @@ export const saveStateToDatabase = async ({
|
|||||||
|
|
||||||
const session = id ? { state, id } : await createSession({ state })
|
const session = id ? { state, id } : await createSession({ state })
|
||||||
|
|
||||||
if (!state?.result?.id) return session
|
const resultId = state.typebotsQueue[0].resultId
|
||||||
|
|
||||||
|
if (!resultId) return session
|
||||||
|
|
||||||
const containsSetVariableClientSideAction = clientSideActions?.some(
|
const containsSetVariableClientSideAction = clientSideActions?.some(
|
||||||
(action) => 'setVariable' in action
|
(action) => 'setVariable' in action
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const answers = state.typebotsQueue[0].answers
|
||||||
|
|
||||||
await upsertResult({
|
await upsertResult({
|
||||||
state,
|
resultId,
|
||||||
|
typebot: state.typebotsQueue[0].typebot,
|
||||||
isCompleted: Boolean(
|
isCompleted: Boolean(
|
||||||
!input &&
|
!input && !containsSetVariableClientSideAction && answers.length > 0
|
||||||
!containsSetVariableClientSideAction &&
|
|
||||||
state.result.answers.length > 0
|
|
||||||
),
|
),
|
||||||
|
hasStarted: answers.length > 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (logs && logs.length > 0)
|
if (logs && logs.length > 0)
|
||||||
await saveLogs(
|
await saveLogs(
|
||||||
logs.map((log) => ({
|
logs.map((log) => ({
|
||||||
...log,
|
...log,
|
||||||
resultId: state.result.id as string,
|
resultId,
|
||||||
details: formatLogDetails(log.details),
|
details: formatLogDetails(log.details),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const startBotFlow = async (
|
|||||||
startGroupId?: string
|
startGroupId?: string
|
||||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||||
if (startGroupId) {
|
if (startGroupId) {
|
||||||
const group = state.typebot.groups.find(
|
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||||
(group) => group.id === startGroupId
|
(group) => group.id === startGroupId
|
||||||
)
|
)
|
||||||
if (!group)
|
if (!group)
|
||||||
@@ -18,9 +18,10 @@ export const startBotFlow = async (
|
|||||||
})
|
})
|
||||||
return executeGroup(state)(group)
|
return executeGroup(state)(group)
|
||||||
}
|
}
|
||||||
const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId
|
const firstEdgeId =
|
||||||
|
state.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
|
||||||
if (!firstEdgeId) return { messages: [], newSessionState: state }
|
if (!firstEdgeId) return { messages: [], newSessionState: state }
|
||||||
const nextGroup = getNextGroup(state)(firstEdgeId)
|
const nextGroup = getNextGroup(state)(firstEdgeId)
|
||||||
if (!nextGroup) return { messages: [], newSessionState: state }
|
if (!nextGroup.group) return { messages: [], newSessionState: state }
|
||||||
return executeGroup(state)(nextGroup.group)
|
return executeGroup(state)(nextGroup.group)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { getDefinedVariables } from '@typebot.io/lib/results'
|
||||||
|
import { TypebotInSession } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
resultId: string
|
||||||
|
typebot: TypebotInSession
|
||||||
|
hasStarted: boolean
|
||||||
|
isCompleted: boolean
|
||||||
|
}
|
||||||
|
export const createResultIfNotExist = async ({
|
||||||
|
resultId,
|
||||||
|
typebot,
|
||||||
|
hasStarted,
|
||||||
|
isCompleted,
|
||||||
|
}: Props) => {
|
||||||
|
const existingResult = await prisma.result.findUnique({
|
||||||
|
where: { id: resultId },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
if (existingResult) return
|
||||||
|
return prisma.result.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: resultId,
|
||||||
|
typebotId: typebot.id,
|
||||||
|
isCompleted: isCompleted ? true : false,
|
||||||
|
hasStarted,
|
||||||
|
variables: getDefinedVariables(typebot.variables),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
import { Answer, Result } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
@@ -9,6 +10,18 @@ export const findResult = ({ id }: Props) =>
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
variables: true,
|
variables: true,
|
||||||
answers: { select: { blockId: true, variableId: true, content: true } },
|
hasStarted: true,
|
||||||
|
answers: {
|
||||||
|
select: {
|
||||||
|
content: true,
|
||||||
|
blockId: true,
|
||||||
|
variableId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
}) as Promise<
|
||||||
|
| (Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
|
||||||
|
answers: Pick<Answer, 'content' | 'blockId' | 'variableId'>[]
|
||||||
|
})
|
||||||
|
| null
|
||||||
|
>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { ChatSession } from '@typebot.io/schemas'
|
import { ChatSession, sessionStateSchema } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const getSession = async (
|
export const getSession = async (
|
||||||
sessionId: string
|
sessionId: string
|
||||||
): Promise<Pick<ChatSession, 'state' | 'id'> | null> => {
|
): Promise<Pick<ChatSession, 'state' | 'id'> | null> => {
|
||||||
const session = (await prisma.chatSession.findUnique({
|
const session = await prisma.chatSession.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
select: { id: true, state: true },
|
select: { id: true, state: true },
|
||||||
})) as Pick<ChatSession, 'state' | 'id'> | null
|
})
|
||||||
return session
|
if (!session) return null
|
||||||
|
return { ...session, state: sessionStateSchema.parse(session.state) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ type Props = {
|
|||||||
state: SessionState
|
state: SessionState
|
||||||
}
|
}
|
||||||
export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
|
export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
|
||||||
if (!state.result?.id) return
|
const resultId = state.typebotsQueue[0].resultId
|
||||||
|
if (!resultId) return
|
||||||
if (reply.includes('http') && block.type === InputBlockType.FILE) {
|
if (reply.includes('http') && block.type === InputBlockType.FILE) {
|
||||||
answer.storageUsed = await computeStorageUsed(reply)
|
answer.storageUsed = await computeStorageUsed(reply)
|
||||||
}
|
}
|
||||||
const where = {
|
const where = {
|
||||||
resultId: state.result.id,
|
resultId,
|
||||||
blockId: block.id,
|
blockId: block.id,
|
||||||
groupId: block.groupId,
|
groupId: block.groupId,
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,7 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return prisma.answer.createMany({
|
return prisma.answer.createMany({
|
||||||
data: [{ ...answer, resultId: state.result.id }],
|
data: [{ ...answer, resultId }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,43 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { SessionState } from '@typebot.io/schemas'
|
import { getDefinedVariables } from '@typebot.io/lib/results'
|
||||||
|
import { TypebotInSession } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: SessionState
|
resultId: string
|
||||||
|
typebot: TypebotInSession
|
||||||
|
hasStarted: boolean
|
||||||
isCompleted: boolean
|
isCompleted: boolean
|
||||||
}
|
}
|
||||||
export const upsertResult = async ({ state, isCompleted }: Props) => {
|
export const upsertResult = async ({
|
||||||
|
resultId,
|
||||||
|
typebot,
|
||||||
|
hasStarted,
|
||||||
|
isCompleted,
|
||||||
|
}: Props) => {
|
||||||
const existingResult = await prisma.result.findUnique({
|
const existingResult = await prisma.result.findUnique({
|
||||||
where: { id: state.result.id },
|
where: { id: resultId },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
})
|
})
|
||||||
|
const variablesWithValue = getDefinedVariables(typebot.variables)
|
||||||
|
|
||||||
if (existingResult) {
|
if (existingResult) {
|
||||||
return prisma.result.updateMany({
|
return prisma.result.updateMany({
|
||||||
where: { id: state.result.id },
|
where: { id: resultId },
|
||||||
data: {
|
data: {
|
||||||
isCompleted: isCompleted ? true : undefined,
|
isCompleted: isCompleted ? true : undefined,
|
||||||
hasStarted: state.result.answers.length > 0 ? true : undefined,
|
hasStarted,
|
||||||
variables: state.result.variables,
|
variables: variablesWithValue,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return prisma.result.createMany({
|
return prisma.result.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: state.result.id,
|
id: resultId,
|
||||||
typebotId: state.typebot.id,
|
typebotId: typebot.id,
|
||||||
isCompleted: isCompleted ? true : false,
|
isCompleted: isCompleted ? true : false,
|
||||||
hasStarted: state.result.answers.length > 0 ? true : undefined,
|
hasStarted,
|
||||||
variables: state.result.variables,
|
variables: variablesWithValue,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { isDefined } from '@typebot.io/lib'
|
|
||||||
import {
|
import {
|
||||||
SessionState,
|
SessionState,
|
||||||
VariableWithUnknowValue,
|
VariableWithUnknowValue,
|
||||||
VariableWithValue,
|
|
||||||
Variable,
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||||
@@ -11,40 +9,23 @@ export const updateVariables =
|
|||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
(newVariables: VariableWithUnknowValue[]): SessionState => ({
|
(newVariables: VariableWithUnknowValue[]): SessionState => ({
|
||||||
...state,
|
...state,
|
||||||
typebot: {
|
typebotsQueue: state.typebotsQueue.map((typebotInQueue, index) =>
|
||||||
...state.typebot,
|
index === 0
|
||||||
variables: updateTypebotVariables(state)(newVariables),
|
? {
|
||||||
},
|
...typebotInQueue,
|
||||||
result: {
|
typebot: {
|
||||||
...state.result,
|
...typebotInQueue.typebot,
|
||||||
variables: updateResultVariables(state)(newVariables),
|
variables: updateTypebotVariables(typebotInQueue.typebot)(
|
||||||
},
|
newVariables
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: typebotInQueue
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateResultVariables =
|
|
||||||
({ result }: Pick<SessionState, 'result' | 'typebot'>) =>
|
|
||||||
(newVariables: VariableWithUnknowValue[]): VariableWithValue[] => {
|
|
||||||
const serializedNewVariables = newVariables.map((variable) => ({
|
|
||||||
...variable,
|
|
||||||
value: Array.isArray(variable.value)
|
|
||||||
? variable.value.map(safeStringify)
|
|
||||||
: safeStringify(variable.value),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const updatedVariables = [
|
|
||||||
...result.variables.filter((existingVariable) =>
|
|
||||||
serializedNewVariables.every(
|
|
||||||
(newVariable) => existingVariable.id !== newVariable.id
|
|
||||||
)
|
|
||||||
),
|
|
||||||
...serializedNewVariables,
|
|
||||||
].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
|
|
||||||
|
|
||||||
return updatedVariables
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTypebotVariables =
|
const updateTypebotVariables =
|
||||||
({ typebot }: Pick<SessionState, 'result' | 'typebot'>) =>
|
(typebot: { variables: Variable[] }) =>
|
||||||
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
||||||
const serializedNewVariables = newVariables.map((variable) => ({
|
const serializedNewVariables = newVariables.map((variable) => ({
|
||||||
...variable,
|
...variable,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const handler = async (req: Request) => {
|
|||||||
|
|
||||||
if (!state) return new Response('No state found', { status: 400 })
|
if (!state) return new Response('No state found', { status: 400 })
|
||||||
|
|
||||||
const group = state.typebot.groups.find(
|
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||||
(group) => group.id === state.currentBlock?.groupId
|
(group) => group.id === state.currentBlock?.groupId
|
||||||
)
|
)
|
||||||
const blockIndex =
|
const blockIndex =
|
||||||
|
|||||||
@@ -85,131 +85,116 @@ const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
|||||||
|
|
||||||
export const executeWebhook =
|
export const executeWebhook =
|
||||||
(typebot: Typebot) =>
|
(typebot: Typebot) =>
|
||||||
async ({
|
async ({
|
||||||
webhook,
|
webhook,
|
||||||
variables,
|
variables,
|
||||||
groupId,
|
groupId,
|
||||||
resultValues,
|
resultValues,
|
||||||
resultId,
|
resultId,
|
||||||
parentTypebotIds = [],
|
parentTypebotIds = [],
|
||||||
}: {
|
}: {
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
groupId: string
|
groupId: string
|
||||||
resultValues?: ResultValues
|
resultValues?: ResultValues
|
||||||
resultId?: string
|
resultId?: string
|
||||||
parentTypebotIds: string[]
|
parentTypebotIds: string[]
|
||||||
}): Promise<WebhookResponse> => {
|
}): Promise<WebhookResponse> => {
|
||||||
if (!webhook.url || !webhook.method)
|
if (!webhook.url || !webhook.method)
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
data: { message: `Webhook doesn't have url or method` },
|
data: { message: `Webhook doesn't have url or method` },
|
||||||
}
|
|
||||||
const basicAuth: { username?: string; password?: string } = {}
|
|
||||||
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
|
||||||
(h) =>
|
|
||||||
h.key?.toLowerCase() === 'authorization' &&
|
|
||||||
h.value?.toLowerCase()?.includes('basic')
|
|
||||||
)
|
|
||||||
const isUsernamePasswordBasicAuth =
|
|
||||||
basicAuthHeaderIdx !== -1 &&
|
|
||||||
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
|
|
||||||
if (isUsernamePasswordBasicAuth) {
|
|
||||||
const [username, password] =
|
|
||||||
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
|
|
||||||
basicAuth.username = username
|
|
||||||
basicAuth.password = password
|
|
||||||
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
|
||||||
}
|
}
|
||||||
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
const basicAuth: { username?: string; password?: string } = {}
|
||||||
| Headers
|
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
||||||
| undefined
|
(h) =>
|
||||||
const queryParams = stringify(
|
h.key?.toLowerCase() === 'authorization' &&
|
||||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
h.value?.toLowerCase()?.includes('basic')
|
||||||
)
|
)
|
||||||
const contentType = headers ? headers['Content-Type'] : undefined
|
const isUsernamePasswordBasicAuth =
|
||||||
const linkedTypebotsParents = await fetchLinkedTypebots({
|
basicAuthHeaderIdx !== -1 &&
|
||||||
isPreview: !('typebotId' in typebot),
|
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
|
||||||
typebotIds: parentTypebotIds,
|
if (isUsernamePasswordBasicAuth) {
|
||||||
})
|
const [username, password] =
|
||||||
const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({
|
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
|
||||||
isPreview: !('typebotId' in typebot),
|
basicAuth.username = username
|
||||||
typebots: [typebot],
|
basicAuth.password = password
|
||||||
})([])
|
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
||||||
const bodyContent = await getBodyContent(typebot, [
|
}
|
||||||
...linkedTypebotsParents,
|
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
||||||
...linkedTypebotsChildren,
|
| Headers
|
||||||
])({
|
| undefined
|
||||||
body: webhook.body,
|
const queryParams = stringify(
|
||||||
resultValues,
|
convertKeyValueTableToObject(webhook.queryParams, variables)
|
||||||
groupId,
|
)
|
||||||
variables,
|
const contentType = headers ? headers['Content-Type'] : undefined
|
||||||
})
|
const linkedTypebotsParents = await fetchLinkedTypebots({
|
||||||
const { data: body, isJson } =
|
isPreview: !('typebotId' in typebot),
|
||||||
bodyContent && webhook.method !== HttpMethod.GET
|
typebotIds: parentTypebotIds,
|
||||||
? safeJsonParse(
|
})
|
||||||
|
const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({
|
||||||
|
isPreview: !('typebotId' in typebot),
|
||||||
|
typebots: [typebot],
|
||||||
|
})([])
|
||||||
|
const bodyContent = await getBodyContent(typebot, [
|
||||||
|
...linkedTypebotsParents,
|
||||||
|
...linkedTypebotsChildren,
|
||||||
|
])({
|
||||||
|
body: webhook.body,
|
||||||
|
resultValues,
|
||||||
|
groupId,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
const { data: body, isJson } =
|
||||||
|
bodyContent && webhook.method !== HttpMethod.GET
|
||||||
|
? safeJsonParse(
|
||||||
parseVariables(variables, {
|
parseVariables(variables, {
|
||||||
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
||||||
})(bodyContent)
|
})(bodyContent)
|
||||||
)
|
)
|
||||||
: { data: undefined, isJson: false }
|
: { data: undefined, isJson: false }
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
url: parseVariables(variables)(
|
url: parseVariables(variables)(
|
||||||
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
||||||
),
|
),
|
||||||
method: webhook.method as Method,
|
method: webhook.method as Method,
|
||||||
headers,
|
headers,
|
||||||
...basicAuth,
|
...basicAuth,
|
||||||
json:
|
json:
|
||||||
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
||||||
? body
|
? body
|
||||||
: undefined,
|
: undefined,
|
||||||
form:
|
form:
|
||||||
contentType?.includes('x-www-form-urlencoded') && body
|
contentType?.includes('x-www-form-urlencoded') && body
|
||||||
? body
|
? body
|
||||||
: undefined,
|
: undefined,
|
||||||
body: body && !isJson ? body : undefined,
|
body: body && !isJson ? body : undefined,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await got(request.url, omit(request, 'url'))
|
const response = await got(request.url, omit(request, 'url'))
|
||||||
await saveSuccessLog({
|
await saveSuccessLog({
|
||||||
resultId,
|
resultId,
|
||||||
message: 'Webhook successfuly executed.',
|
message: 'Webhook successfuly executed.',
|
||||||
details: {
|
details: {
|
||||||
statusCode: response.statusCode,
|
|
||||||
request,
|
|
||||||
response: safeJsonParse(response.body).data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
data: safeJsonParse(response.body).data,
|
request,
|
||||||
}
|
response: safeJsonParse(response.body).data,
|
||||||
} catch (error) {
|
},
|
||||||
if (error instanceof HTTPError) {
|
})
|
||||||
const response = {
|
return {
|
||||||
statusCode: error.response.statusCode,
|
statusCode: response.statusCode,
|
||||||
data: safeJsonParse(error.response.body as string).data,
|
data: safeJsonParse(response.body).data,
|
||||||
}
|
}
|
||||||
await saveErrorLog({
|
} catch (error) {
|
||||||
resultId,
|
if (error instanceof HTTPError) {
|
||||||
message: 'Webhook returned an error',
|
|
||||||
details: {
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
const response = {
|
const response = {
|
||||||
statusCode: 500,
|
statusCode: error.response.statusCode,
|
||||||
data: { message: `Error from Typebot server: ${error}` },
|
data: safeJsonParse(error.response.body as string).data,
|
||||||
}
|
}
|
||||||
console.error(error)
|
|
||||||
await saveErrorLog({
|
await saveErrorLog({
|
||||||
resultId,
|
resultId,
|
||||||
message: 'Webhook failed to execute',
|
message: 'Webhook returned an error',
|
||||||
details: {
|
details: {
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
@@ -217,36 +202,66 @@ export const executeWebhook =
|
|||||||
})
|
})
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
const response = {
|
||||||
|
statusCode: 500,
|
||||||
|
data: { message: `Error from Typebot server: ${error}` },
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
await saveErrorLog({
|
||||||
|
resultId,
|
||||||
|
message: 'Webhook failed to execute',
|
||||||
|
details: {
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getBodyContent =
|
const getBodyContent =
|
||||||
(
|
(
|
||||||
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
||||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||||
) =>
|
) =>
|
||||||
async ({
|
async ({
|
||||||
body,
|
body,
|
||||||
resultValues,
|
resultValues,
|
||||||
groupId,
|
groupId,
|
||||||
variables,
|
variables,
|
||||||
}: {
|
}: {
|
||||||
body?: string | null
|
body?: string | null
|
||||||
resultValues?: ResultValues
|
resultValues?: ResultValues
|
||||||
groupId: string
|
groupId: string
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
}): Promise<string | undefined> => {
|
}): Promise<string | undefined> => {
|
||||||
if (!body) return
|
if (!body) return
|
||||||
return body === '{{state}}'
|
return body === '{{state}}'
|
||||||
? JSON.stringify(
|
? JSON.stringify(
|
||||||
resultValues
|
resultValues
|
||||||
? parseAnswers(typebot, linkedTypebots)(resultValues)
|
? parseAnswers({
|
||||||
|
answers: resultValues.answers.map((answer) => ({
|
||||||
|
key:
|
||||||
|
(answer.variableId
|
||||||
|
? typebot.variables.find(
|
||||||
|
(variable) => variable.id === answer.variableId
|
||||||
|
)?.name
|
||||||
|
: typebot.groups.find((group) =>
|
||||||
|
group.blocks.find(
|
||||||
|
(block) => block.id === answer.blockId
|
||||||
|
)
|
||||||
|
)?.title) ?? '',
|
||||||
|
value: answer.content,
|
||||||
|
})),
|
||||||
|
variables: resultValues.variables,
|
||||||
|
})
|
||||||
: await parseSampleResult(typebot, linkedTypebots)(
|
: await parseSampleResult(typebot, linkedTypebots)(
|
||||||
groupId,
|
groupId,
|
||||||
variables
|
variables
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: body
|
: body
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertKeyValueTableToObject = (
|
const convertKeyValueTableToObject = (
|
||||||
keyValues: KeyValue[] | undefined,
|
keyValues: KeyValue[] | undefined,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import Mail from 'nodemailer/lib/mailer'
|
|||||||
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
||||||
import { render } from '@faire/mjml-react/utils/render'
|
import { render } from '@faire/mjml-react/utils/render'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
|
|
||||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||||
|
|
||||||
@@ -197,10 +196,20 @@ const getEmailBody = async ({
|
|||||||
where: { typebotId },
|
where: { typebotId },
|
||||||
})) as unknown as PublicTypebot
|
})) as unknown as PublicTypebot
|
||||||
if (!typebot) return
|
if (!typebot) return
|
||||||
const linkedTypebots = await getPreviouslyLinkedTypebots({
|
const answers = parseAnswers({
|
||||||
typebots: [typebot],
|
answers: resultValues.answers.map((answer) => ({
|
||||||
})([])
|
key:
|
||||||
const answers = parseAnswers(typebot, linkedTypebots)(resultValues)
|
(answer.variableId
|
||||||
|
? typebot.variables.find(
|
||||||
|
(variable) => variable.id === answer.variableId
|
||||||
|
)?.name
|
||||||
|
: typebot.groups.find((group) =>
|
||||||
|
group.blocks.find((block) => block.id === answer.blockId)
|
||||||
|
)?.title) ?? '',
|
||||||
|
value: answer.content,
|
||||||
|
})),
|
||||||
|
variables: resultValues.variables,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
html: render(
|
html: render(
|
||||||
<DefaultBotNotificationEmail
|
<DefaultBotNotificationEmail
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"id": "cl0ibhi7s0018n21aarlag0cm",
|
||||||
|
"createdAt": "2022-03-08T15:58:49.720Z",
|
||||||
|
"updatedAt": "2022-03-08T16:07:18.899Z",
|
||||||
|
"name": "My typebot",
|
||||||
|
"folderId": null,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "1qQrnsLzRim1LqCrhbj1MW",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "8srsGhdBJK8v88Xo1RRS4C",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"groupId": "1qQrnsLzRim1LqCrhbj1MW",
|
||||||
|
"outgoingEdgeId": "ovUHhwr6THMhqtn8QbkjtA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Start",
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wSR4VCcDNDTTsD9Szi2xH8",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "sw6nHJfkMsM4pxZxMBB6QqW",
|
||||||
|
"type": "Typebot link",
|
||||||
|
"groupId": "wSR4VCcDNDTTsD9Szi2xH8",
|
||||||
|
"options": {
|
||||||
|
"typebotId": "cl0ibhv8d0130n21aw8doxhj5",
|
||||||
|
"mergeResults": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Group #1",
|
||||||
|
"graphCoordinates": { "x": 363, "y": 199 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"id": "ovUHhwr6THMhqtn8QbkjtA",
|
||||||
|
"to": { "groupId": "wSR4VCcDNDTTsD9Szi2xH8" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "8srsGhdBJK8v88Xo1RRS4C",
|
||||||
|
"groupId": "1qQrnsLzRim1LqCrhbj1MW"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"chat": {
|
||||||
|
"inputs": {
|
||||||
|
"color": "#303235",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"placeholderColor": "#9095A0"
|
||||||
|
},
|
||||||
|
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||||
|
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||||
|
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||||
|
},
|
||||||
|
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"isBrandingEnabled": true,
|
||||||
|
"isNewResultOnRefreshEnabled": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||||
|
},
|
||||||
|
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||||
|
},
|
||||||
|
"publicId": null,
|
||||||
|
"customDomain": null
|
||||||
|
}
|
||||||
@@ -3,12 +3,11 @@ import {
|
|||||||
Variable,
|
Variable,
|
||||||
InputBlock,
|
InputBlock,
|
||||||
ResultHeaderCell,
|
ResultHeaderCell,
|
||||||
Answer,
|
|
||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
Typebot,
|
Typebot,
|
||||||
ResultWithAnswers,
|
ResultWithAnswers,
|
||||||
InputBlockType,
|
InputBlockType,
|
||||||
ResultInSession,
|
AnswerInSessionState,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import {
|
import {
|
||||||
isInputBlock,
|
isInputBlock,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
byId,
|
byId,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
parseGroupTitle,
|
parseGroupTitle,
|
||||||
|
isEmpty,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
export const parseResultHeader = (
|
export const parseResultHeader = (
|
||||||
@@ -216,50 +216,36 @@ const parseResultsFromPreviousBotVersions = (
|
|||||||
]
|
]
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
export const parseAnswers =
|
export const parseAnswers = ({
|
||||||
(
|
answers,
|
||||||
typebot: Pick<Typebot, 'groups' | 'variables'>,
|
variables: resultVariables,
|
||||||
linkedTypebots: Pick<Typebot, 'groups' | 'variables'>[] | undefined
|
}: {
|
||||||
) =>
|
answers: AnswerInSessionState[]
|
||||||
({
|
variables: VariableWithValue[]
|
||||||
createdAt,
|
}): {
|
||||||
answers,
|
[key: string]: string
|
||||||
variables: resultVariables,
|
} => {
|
||||||
}: Omit<ResultInSession, 'hasStarted'> & { createdAt?: Date | string }): {
|
return {
|
||||||
[key: string]: string
|
submittedAt: new Date().toISOString(),
|
||||||
} => {
|
...[...answers, ...resultVariables].reduce<{
|
||||||
const header = parseResultHeader(typebot, linkedTypebots)
|
[key: string]: string
|
||||||
return {
|
}>((o, answerOrVariable) => {
|
||||||
submittedAt: !createdAt
|
if ('id' in answerOrVariable) {
|
||||||
? new Date().toISOString()
|
const variable = answerOrVariable
|
||||||
: typeof createdAt === 'string'
|
if (variable.value === null) return o
|
||||||
? createdAt
|
return { ...o, [variable.name]: variable.value.toString() }
|
||||||
: createdAt.toISOString(),
|
}
|
||||||
...[...answers, ...resultVariables].reduce<{
|
const answer = answerOrVariable as AnswerInSessionState
|
||||||
[key: string]: string
|
if (isEmpty(answer.key)) return o
|
||||||
}>((o, answerOrVariable) => {
|
return {
|
||||||
const isVariable = !('blockId' in answerOrVariable)
|
...o,
|
||||||
if (isVariable) {
|
[answer.key]: answer.value,
|
||||||
const variable = answerOrVariable as VariableWithValue
|
}
|
||||||
if (variable.value === null) return o
|
}, {}),
|
||||||
return { ...o, [variable.name]: variable.value.toString() }
|
|
||||||
}
|
|
||||||
const answer = answerOrVariable as Answer
|
|
||||||
const key = answer.variableId
|
|
||||||
? header.find(
|
|
||||||
(cell) =>
|
|
||||||
answer.variableId &&
|
|
||||||
cell.variableIds?.includes(answer.variableId)
|
|
||||||
)?.label
|
|
||||||
: header.find((cell) =>
|
|
||||||
cell.blocks?.some((block) => block.id === answer.blockId)
|
|
||||||
)?.label
|
|
||||||
if (!key) return o
|
|
||||||
if (isDefined(o[key])) return o
|
|
||||||
return {
|
|
||||||
...o,
|
|
||||||
[key]: answer.content.toString(),
|
|
||||||
}
|
|
||||||
}, {}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefinedVariables = (variables: Variable[]) =>
|
||||||
|
variables.filter((variable) =>
|
||||||
|
isDefined(variable.value)
|
||||||
|
) as VariableWithValue[]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { LogicBlockType } from './enums'
|
|||||||
export const typebotLinkOptionsSchema = z.object({
|
export const typebotLinkOptionsSchema = z.object({
|
||||||
typebotId: z.string().optional(),
|
typebotId: z.string().optional(),
|
||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
|
mergeResults: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const typebotLinkBlockSchema = blockBaseSchema.merge(
|
export const typebotLinkBlockSchema = blockBaseSchema.merge(
|
||||||
@@ -14,7 +15,9 @@ export const typebotLinkBlockSchema = blockBaseSchema.merge(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultTypebotLinkOptions: TypebotLinkOptions = {}
|
export const defaultTypebotLinkOptions: TypebotLinkOptions = {
|
||||||
|
mergeResults: false,
|
||||||
|
}
|
||||||
|
|
||||||
export type TypebotLinkBlock = z.infer<typeof typebotLinkBlockSchema>
|
export type TypebotLinkBlock = z.infer<typeof typebotLinkBlockSchema>
|
||||||
export type TypebotLinkOptions = z.infer<typeof typebotLinkOptionsSchema>
|
export type TypebotLinkOptions = z.infer<typeof typebotLinkOptionsSchema>
|
||||||
|
|||||||
@@ -5,68 +5,21 @@ import {
|
|||||||
paymentInputRuntimeOptionsSchema,
|
paymentInputRuntimeOptionsSchema,
|
||||||
pixelOptionsSchema,
|
pixelOptionsSchema,
|
||||||
redirectOptionsSchema,
|
redirectOptionsSchema,
|
||||||
} from './blocks'
|
} from '../blocks'
|
||||||
import { publicTypebotSchema } from './publicTypebot'
|
import { logSchema } from '../result'
|
||||||
import { logSchema, resultSchema } from './result'
|
import { listVariableValue, typebotSchema } from '../typebot'
|
||||||
import { listVariableValue, typebotSchema } from './typebot'
|
|
||||||
import {
|
import {
|
||||||
textBubbleContentSchema,
|
textBubbleContentSchema,
|
||||||
imageBubbleContentSchema,
|
imageBubbleContentSchema,
|
||||||
videoBubbleContentSchema,
|
videoBubbleContentSchema,
|
||||||
audioBubbleContentSchema,
|
audioBubbleContentSchema,
|
||||||
embedBubbleContentSchema,
|
embedBubbleContentSchema,
|
||||||
} from './blocks/bubbles'
|
} from '../blocks/bubbles'
|
||||||
import { answerSchema } from './answer'
|
import { BubbleBlockType } from '../blocks/bubbles/enums'
|
||||||
import { BubbleBlockType } from './blocks/bubbles/enums'
|
import { inputBlockSchemas } from '../blocks/schemas'
|
||||||
import { inputBlockSchemas } from './blocks/schemas'
|
import { chatCompletionMessageSchema } from '../blocks/integrations/openai'
|
||||||
import { chatCompletionMessageSchema } from './blocks/integrations/openai'
|
import { sessionStateSchema } from './sessionState'
|
||||||
|
import { dynamicThemeSchema } from './shared'
|
||||||
const typebotInSessionStateSchema = publicTypebotSchema._def.schema.pick({
|
|
||||||
id: true,
|
|
||||||
groups: true,
|
|
||||||
edges: true,
|
|
||||||
variables: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const dynamicThemeSchema = z.object({
|
|
||||||
hostAvatarUrl: z.string().optional(),
|
|
||||||
guestAvatarUrl: z.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const answerInSessionStateSchema = answerSchema.pick({
|
|
||||||
content: true,
|
|
||||||
blockId: true,
|
|
||||||
variableId: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const resultInSessionStateSchema = resultSchema
|
|
||||||
.pick({
|
|
||||||
variables: true,
|
|
||||||
})
|
|
||||||
.merge(
|
|
||||||
z.object({
|
|
||||||
answers: z.array(answerInSessionStateSchema),
|
|
||||||
id: z.string().optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const sessionStateSchema = z.object({
|
|
||||||
typebot: typebotInSessionStateSchema,
|
|
||||||
dynamicTheme: dynamicThemeSchema.optional(),
|
|
||||||
linkedTypebots: z.object({
|
|
||||||
typebots: z.array(typebotInSessionStateSchema),
|
|
||||||
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
|
|
||||||
}),
|
|
||||||
currentTypebotId: z.string(),
|
|
||||||
result: resultInSessionStateSchema,
|
|
||||||
currentBlock: z
|
|
||||||
.object({
|
|
||||||
blockId: z.string(),
|
|
||||||
groupId: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
isStreamEnabled: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const chatSessionSchema = z.object({
|
const chatSessionSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -301,9 +254,7 @@ export const chatReplySchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type ChatSession = z.infer<typeof chatSessionSchema>
|
export type ChatSession = z.infer<typeof chatSessionSchema>
|
||||||
export type SessionState = z.infer<typeof sessionStateSchema>
|
|
||||||
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
|
|
||||||
export type ResultInSession = z.infer<typeof resultInSessionStateSchema>
|
|
||||||
export type ChatReply = z.infer<typeof chatReplySchema>
|
export type ChatReply = z.infer<typeof chatReplySchema>
|
||||||
export type ChatMessage = z.infer<typeof chatMessageSchema>
|
export type ChatMessage = z.infer<typeof chatMessageSchema>
|
||||||
export type SendMessageInput = z.infer<typeof sendMessageInputSchema>
|
export type SendMessageInput = z.infer<typeof sendMessageInputSchema>
|
||||||
125
packages/schemas/features/chat/sessionState.ts
Normal file
125
packages/schemas/features/chat/sessionState.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { answerSchema } from '../answer'
|
||||||
|
import { resultSchema } from '../result'
|
||||||
|
import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared'
|
||||||
|
|
||||||
|
const answerInSessionStateSchema = answerSchema.pick({
|
||||||
|
content: true,
|
||||||
|
blockId: true,
|
||||||
|
variableId: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const answerInSessionStateSchemaV2 = z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AnswerInSessionState = z.infer<typeof answerInSessionStateSchemaV2>
|
||||||
|
|
||||||
|
const resultInSessionStateSchema = resultSchema
|
||||||
|
.pick({
|
||||||
|
variables: true,
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
answers: z.array(answerInSessionStateSchema),
|
||||||
|
id: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionStateSchemaV1 = z.object({
|
||||||
|
typebot: typebotInSessionStateSchema,
|
||||||
|
dynamicTheme: dynamicThemeSchema.optional(),
|
||||||
|
linkedTypebots: z.object({
|
||||||
|
typebots: z.array(typebotInSessionStateSchema),
|
||||||
|
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
|
||||||
|
}),
|
||||||
|
currentTypebotId: z.string(),
|
||||||
|
result: resultInSessionStateSchema,
|
||||||
|
currentBlock: z
|
||||||
|
.object({
|
||||||
|
blockId: z.string(),
|
||||||
|
groupId: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
isStreamEnabled: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessionStateSchemaV2 = z.object({
|
||||||
|
version: z.literal('2'),
|
||||||
|
typebotsQueue: z.array(
|
||||||
|
z.object({
|
||||||
|
edgeIdToTriggerWhenDone: z.string().optional(),
|
||||||
|
isMergingWithParent: z.boolean().optional(),
|
||||||
|
resultId: z.string().optional(),
|
||||||
|
answers: z.array(answerInSessionStateSchemaV2),
|
||||||
|
typebot: typebotInSessionStateSchema,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
dynamicTheme: dynamicThemeSchema.optional(),
|
||||||
|
currentBlock: z
|
||||||
|
.object({
|
||||||
|
blockId: z.string(),
|
||||||
|
groupId: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
isStreamEnabled: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SessionState = z.infer<typeof sessionStateSchemaV2>
|
||||||
|
|
||||||
|
export const sessionStateSchema = sessionStateSchemaV1
|
||||||
|
.or(sessionStateSchemaV2)
|
||||||
|
.transform((state): SessionState => {
|
||||||
|
if ('version' in state) return state
|
||||||
|
return {
|
||||||
|
version: '2',
|
||||||
|
typebotsQueue: [
|
||||||
|
{
|
||||||
|
typebot: state.typebot,
|
||||||
|
resultId: state.result.id,
|
||||||
|
answers: state.result.answers.map((answer) => ({
|
||||||
|
key:
|
||||||
|
(answer.variableId
|
||||||
|
? state.typebot.variables.find(
|
||||||
|
(variable) => variable.id === answer.variableId
|
||||||
|
)?.name
|
||||||
|
: state.typebot.groups.find((group) =>
|
||||||
|
group.blocks.find((block) => block.id === answer.blockId)
|
||||||
|
)?.title) ?? '',
|
||||||
|
value: answer.content,
|
||||||
|
})),
|
||||||
|
isMergingWithParent: true,
|
||||||
|
edgeIdToTriggerWhenDone:
|
||||||
|
state.linkedTypebots.queue.length > 0
|
||||||
|
? state.linkedTypebots.queue[0].edgeId
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
...state.linkedTypebots.typebots.map(
|
||||||
|
(typebot, index) =>
|
||||||
|
({
|
||||||
|
typebot,
|
||||||
|
resultId: state.result.id,
|
||||||
|
answers: state.result.answers.map((answer) => ({
|
||||||
|
key:
|
||||||
|
(answer.variableId
|
||||||
|
? state.typebot.variables.find(
|
||||||
|
(variable) => variable.id === answer.variableId
|
||||||
|
)?.name
|
||||||
|
: state.typebot.groups.find((group) =>
|
||||||
|
group.blocks.find(
|
||||||
|
(block) => block.id === answer.blockId
|
||||||
|
)
|
||||||
|
)?.title) ?? '',
|
||||||
|
value: answer.content,
|
||||||
|
})),
|
||||||
|
edgeIdToTriggerWhenDone: state.linkedTypebots.queue.at(index + 1)
|
||||||
|
?.edgeId,
|
||||||
|
} satisfies SessionState['typebotsQueue'][number])
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dynamicTheme: state.dynamicTheme,
|
||||||
|
currentBlock: state.currentBlock,
|
||||||
|
isStreamEnabled: state.isStreamEnabled,
|
||||||
|
}
|
||||||
|
})
|
||||||
17
packages/schemas/features/chat/shared.ts
Normal file
17
packages/schemas/features/chat/shared.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { publicTypebotSchema } from '../publicTypebot'
|
||||||
|
|
||||||
|
export const typebotInSessionStateSchema = publicTypebotSchema._def.schema.pick(
|
||||||
|
{
|
||||||
|
id: true,
|
||||||
|
groups: true,
|
||||||
|
edges: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
|
||||||
|
|
||||||
|
export const dynamicThemeSchema = z.object({
|
||||||
|
hostAvatarUrl: z.string().optional(),
|
||||||
|
guestAvatarUrl: z.string().optional(),
|
||||||
|
})
|
||||||
@@ -5,6 +5,8 @@ export * from './features/result'
|
|||||||
export * from './features/answer'
|
export * from './features/answer'
|
||||||
export * from './features/utils'
|
export * from './features/utils'
|
||||||
export * from './features/credentials'
|
export * from './features/credentials'
|
||||||
export * from './features/chat'
|
export * from './features/chat/schema'
|
||||||
|
export * from './features/chat/sessionState'
|
||||||
|
export * from './features/chat/shared'
|
||||||
export * from './features/workspace'
|
export * from './features/workspace'
|
||||||
export * from './features/items'
|
export * from './features/items'
|
||||||
|
|||||||
Reference in New Issue
Block a user