2
0

🧑‍💻 (chat) Introduce startChat and continueChat endpoints

Closes #1030
This commit is contained in:
Baptiste Arnaud
2023-11-13 15:27:36 +01:00
parent 63233eb7ee
commit 084588a086
74 changed files with 28426 additions and 645 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.2.15",
"version": "0.2.16",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",

View File

@@ -1,7 +1,7 @@
import { LiteBadge } from './LiteBadge'
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { isNotDefined, isNotEmpty } from '@typebot.io/lib'
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
import { startChatQuery } from '@/queries/startChatQuery'
import { ConversationContainer } from './ConversationContainer'
import { setIsMobile } from '@/utils/isMobileSignal'
import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
@@ -12,7 +12,8 @@ import {
} from '@/utils/storage'
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css'
import { InputBlock, StartElementId } from '@typebot.io/schemas'
import { InputBlock } from '@typebot.io/schemas'
import { StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
export type BotProps = {
@@ -27,7 +28,8 @@ export type BotProps = {
onInit?: () => void
onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void
} & StartElementId
startFrom?: StartFrom
}
export const Bot = (props: BotProps & { class?: string }) => {
const [initialChatReply, setInitialChatReply] = createSignal<
@@ -47,11 +49,13 @@ export const Bot = (props: BotProps & { class?: string }) => {
})
const typebotIdFromProps =
typeof props.typebot === 'string' ? props.typebot : undefined
const { data, error } = await getInitialChatReplyQuery({
const isPreview =
typeof props.typebot !== 'string' || (props.isPreview ?? false)
const { data, error } = await startChatQuery({
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
typebot: props.typebot,
apiHost: props.apiHost,
isPreview: props.isPreview ?? false,
isPreview,
resultId: isNotEmpty(props.resultId)
? props.resultId
: getExistingResultIdFromStorage(typebotIdFromProps),
@@ -59,14 +63,10 @@ export const Bot = (props: BotProps & { class?: string }) => {
...prefilledVariables,
...props.prefilledVariables,
},
...('startGroupId' in props
? { startGroupId: props.startGroupId }
: 'startEventId' in props
? { startEventId: props.startEventId }
: {}),
startFrom: props.startFrom,
})
if (error && 'code' in error && typeof error.code === 'string') {
if (typeof props.typebot !== 'string' || (props.isPreview ?? false)) {
if (isPreview) {
return setError(
new Error('An error occurred while loading the bot.', {
cause: error.message,

View File

@@ -1,6 +1,6 @@
import { BotContext, ChatChunk as ChatChunkType } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { ChatReply, Settings, Theme } from '@typebot.io/schemas'
import { ContinueChatResponse, Settings, Theme } from '@typebot.io/schemas'
import { createSignal, For, onMount, Show } from 'solid-js'
import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock'
@@ -9,7 +9,7 @@ import { StreamingBubble } from '../bubbles/StreamingBubble'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
type Props = Pick<ChatReply, 'messages' | 'input'> & {
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
theme: Theme
settings: Settings
inputIndex: number

View File

@@ -1,8 +1,8 @@
import {
ChatReply,
ContinueChatResponse,
InputBlock,
SendMessageInput,
Theme,
ChatLog,
} from '@typebot.io/schemas'
import {
createEffect,
@@ -12,7 +12,7 @@ import {
onMount,
Show,
} from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery'
import { continueChatQuery } from '@/queries/continueChatQuery'
import { ChatChunk } from './ChatChunk'
import {
BotContext,
@@ -30,10 +30,11 @@ import {
setFormattedMessages,
} from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
const parseDynamicTheme = (
initialTheme: Theme,
dynamicTheme: ChatReply['dynamicTheme']
dynamicTheme: ContinueChatResponse['dynamicTheme']
): Theme => ({
...initialTheme,
chat: {
@@ -74,7 +75,7 @@ export const ConversationContainer = (props: Props) => {
},
])
const [dynamicTheme, setDynamicTheme] = createSignal<
ChatReply['dynamicTheme']
ContinueChatResponse['dynamicTheme']
>(props.initialChatReply.dynamicTheme)
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
const [isSending, setIsSending] = createSignal(false)
@@ -136,9 +137,16 @@ export const ConversationContainer = (props: Props) => {
const sendMessage = async (
message: string | undefined,
clientLogs?: SendMessageInput['clientLogs']
clientLogs?: ChatLog[]
) => {
if (clientLogs) props.onNewLogs?.(clientLogs)
if (clientLogs) {
props.onNewLogs?.(clientLogs)
await saveClientLogsQuery({
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
clientLogs,
})
}
setHasError(false)
const currentInputBlock = [...chatChunks()].pop()?.input
if (currentInputBlock?.id && props.onAnswer && message)
@@ -153,11 +161,10 @@ export const ConversationContainer = (props: Props) => {
const longRequest = setTimeout(() => {
setIsSending(true)
}, 1000)
const { data, error } = await sendMessageQuery({
const { data, error } = await continueChatQuery({
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
message,
clientLogs,
})
clearTimeout(longRequest)
setIsSending(false)

View File

@@ -1,5 +1,5 @@
import type {
ChatReply,
ContinueChatResponse,
ChoiceInputBlock,
EmailInputBlock,
FileInputBlock,
@@ -39,7 +39,7 @@ import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constan
type Props = {
ref: HTMLDivElement | undefined
block: NonNullable<ChatReply['input']>
block: NonNullable<ContinueChatResponse['input']>
hasHostAvatar: boolean
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
inputIndex: number
@@ -113,7 +113,7 @@ export const InputChatBlock = (props: Props) => {
const Input = (props: {
context: BotContext
block: NonNullable<ChatReply['input']>
block: NonNullable<ContinueChatResponse['input']>
inputIndex: number
isInputPrefillEnabled: boolean
onSubmit: (answer: InputSubmitContent) => void
@@ -252,11 +252,11 @@ const Input = (props: {
}
const isButtonsBlock = (
block: ChatReply['input']
block: ContinueChatResponse['input']
): ChoiceInputBlock | undefined =>
block?.type === InputBlockType.CHOICE ? block : undefined
const isPictureChoiceBlock = (
block: ChatReply['input']
block: ContinueChatResponse['input']
): PictureChoiceBlock | undefined =>
block?.type === InputBlockType.PICTURE_CHOICE ? block : undefined

View File

@@ -10,7 +10,7 @@ export const defaultBotProps: BotProps = {
onInit: undefined,
onNewLogs: undefined,
isPreview: undefined,
startGroupId: undefined,
startFrom: undefined,
prefilledVariables: undefined,
apiHost: undefined,
resultId: undefined,

View File

@@ -0,0 +1,22 @@
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
import { ContinueChatResponse } from '@typebot.io/schemas'
export const continueChatQuery = ({
apiHost,
message,
sessionId,
}: {
apiHost?: string
message: string | undefined
sessionId: string
}) =>
sendRequest<ContinueChatResponse>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/sessions/${sessionId}/continueChat`,
body: {
message,
},
})

View File

@@ -1,74 +0,0 @@
import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost'
import type {
SendMessageInput,
StartElementId,
StartParams,
} from '@typebot.io/schemas'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib'
import {
getPaymentInProgressInStorage,
removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
export async function getInitialChatReplyQuery({
typebot,
isPreview,
apiHost,
prefilledVariables,
resultId,
stripeRedirectStatus,
...props
}: StartParams & {
stripeRedirectStatus?: string
apiHost?: string
} & StartElementId) {
if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages')
const paymentInProgressStateStr = getPaymentInProgressInStorage() ?? undefined
const paymentInProgressState = paymentInProgressStateStr
? (JSON.parse(paymentInProgressStateStr) as {
sessionId: string
typebot: BotContext['typebot']
})
: undefined
if (paymentInProgressState) removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body: {
startParams: paymentInProgressState
? undefined
: {
isPreview,
typebot,
prefilledVariables,
resultId,
isStreamEnabled: true,
startGroupId:
'startGroupId' in props ? props.startGroupId : undefined,
startEventId:
'startEventId' in props ? props.startEventId : undefined,
},
sessionId: paymentInProgressState?.sessionId,
message: paymentInProgressState
? stripeRedirectStatus === 'failed'
? 'fail'
: 'Success'
: undefined,
} satisfies SendMessageInput,
})
return {
data: data
? {
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
}
: undefined,
error,
}
}

View File

@@ -0,0 +1,22 @@
import { guessApiHost } from '@/utils/guessApiHost'
import type { ChatLog } from '@typebot.io/schemas'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
export const saveClientLogsQuery = ({
apiHost,
sessionId,
clientLogs,
}: {
apiHost?: string
sessionId: string
clientLogs: ChatLog[]
}) =>
sendRequest({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/sessions/${sessionId}/clientLogs`,
body: {
clientLogs,
},
})

View File

@@ -1,13 +0,0 @@
import { guessApiHost } from '@/utils/guessApiHost'
import type { ChatReply, SendMessageInput } from '@typebot.io/schemas'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
export const sendMessageQuery = ({
apiHost,
...body
}: SendMessageInput & { apiHost?: string }) =>
sendRequest<ChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body,
})

View File

@@ -0,0 +1,104 @@
import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib'
import {
getPaymentInProgressInStorage,
removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
import {
StartChatInput,
StartFrom,
StartPreviewChatInput,
} from '@typebot.io/schemas'
export async function startChatQuery({
typebot,
isPreview,
apiHost,
prefilledVariables,
resultId,
stripeRedirectStatus,
startFrom,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typebot: string | any
stripeRedirectStatus?: string
apiHost?: string
startFrom?: StartFrom
isPreview: boolean
prefilledVariables?: Record<string, unknown>
resultId?: string
}) {
if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages')
const paymentInProgressStateStr = getPaymentInProgressInStorage() ?? undefined
const paymentInProgressState = paymentInProgressStateStr
? (JSON.parse(paymentInProgressStateStr) as {
sessionId: string
typebot: BotContext['typebot']
})
: undefined
if (paymentInProgressState) {
removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${
paymentInProgressState.sessionId
}/continueChat`,
body: {
message: paymentInProgressState
? stripeRedirectStatus === 'failed'
? 'fail'
: 'Success'
: undefined,
},
})
return {
data: data
? {
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
}
: undefined,
error,
}
}
const typebotId = typeof typebot === 'string' ? typebot : typebot.id
if (isPreview) {
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/typebots/${typebotId}/preview/startChat`,
body: {
isStreamEnabled: true,
startFrom,
typebot,
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
})
return {
data,
error,
}
}
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/typebots/${typebotId}/startChat`,
body: {
isStreamEnabled: true,
prefilledVariables,
resultId,
} satisfies Omit<StartChatInput, 'publicId'>,
})
return {
data,
error,
}
}

View File

@@ -1,4 +1,4 @@
import type { ChatReply } from '@typebot.io/schemas'
import { ContinueChatResponse, StartChatResponse } from '@typebot.io/schemas'
export type InputSubmitContent = {
label?: string
@@ -13,9 +13,9 @@ export type BotContext = {
sessionId: string
}
export type InitialChatReply = ChatReply & {
typebot: NonNullable<ChatReply['typebot']>
sessionId: NonNullable<ChatReply['sessionId']>
export type InitialChatReply = StartChatResponse & {
typebot: NonNullable<StartChatResponse['typebot']>
sessionId: NonNullable<StartChatResponse['sessionId']>
}
export type OutgoingLog = {
@@ -30,7 +30,7 @@ export type ClientSideActionContext = {
}
export type ChatChunk = Pick<
ChatReply,
ContinueChatResponse,
'messages' | 'input' | 'clientSideActions'
> & {
streamingMessageId?: string

View File

@@ -8,11 +8,11 @@ import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
import { executePixel } from '@/features/blocks/integrations/pixel/executePixel'
import { ClientSideActionContext } from '@/types'
import type { ChatReply, ReplyLog } from '@typebot.io/schemas'
import type { ContinueChatResponse, ChatLog } from '@typebot.io/schemas'
import { injectStartProps } from './injectStartProps'
type Props = {
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0]
clientSideAction: NonNullable<ContinueChatResponse['clientSideActions']>[0]
context: ClientSideActionContext
onMessageStream?: (props: { id: string; message: string }) => void
}
@@ -23,7 +23,7 @@ export const executeClientSideAction = async ({
onMessageStream,
}: Props): Promise<
| { blockedPopupUrl: string }
| { replyToSend: string | undefined; logs?: ReplyLog[] }
| { replyToSend: string | undefined; logs?: ChatLog[] }
| void
> => {
if ('chatwoot' in clientSideAction) {

View File

@@ -1,6 +1,6 @@
{
"name": "@typebot.io/nextjs",
"version": "0.2.15",
"version": "0.2.16",
"description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.2.15",
"version": "0.2.16",
"description": "Convenient library to display typebots on your React app",
"main": "dist/index.js",
"types": "dist/index.d.ts",