⚡ (chat) Improve chat API compatibility with preview mode
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -21,8 +21,18 @@ The Chat API allows you to execute (chat) with a typebot.
|
|||||||
|
|
||||||
### How to find my `typebotId`
|
### How to find my `typebotId`
|
||||||
|
|
||||||
|
If you'd like to execute the typebot in preview mode, you will need to provide the ID of the building typebot available in the editor URL:
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src="/img/api/typebotId.png"
|
src="/img/api/typebotId.png"
|
||||||
width="900"
|
width="900"
|
||||||
alt="Get typebot ID"
|
alt="Get typebot ID"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
For published typebot execution, you need to provide the public typebot ID available here:
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/api/publicId.png"
|
||||||
|
width="900"
|
||||||
|
alt="Get typebot ID"
|
||||||
|
/>
|
||||||
|
BIN
apps/docs/static/img/api/publicId.png
vendored
Normal file
BIN
apps/docs/static/img/api/publicId.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
@ -1,33 +1,22 @@
|
|||||||
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
|
||||||
import {
|
|
||||||
getExistingResultFromSession,
|
|
||||||
setResultInSession,
|
|
||||||
} from '@/utils/sessionStorage'
|
|
||||||
import { Standard } from '@typebot.io/react'
|
import { Standard } from '@typebot.io/react'
|
||||||
import { BackgroundType, InitialChatReply, Typebot } from 'models'
|
import { BackgroundType, Typebot } from 'models'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { ErrorPage } from './ErrorPage'
|
|
||||||
import { SEO } from './Seo'
|
import { SEO } from './Seo'
|
||||||
|
|
||||||
export type TypebotPageV2Props = {
|
export type TypebotPageV2Props = {
|
||||||
url: string
|
url: string
|
||||||
typebot: Pick<
|
typebot: Pick<
|
||||||
Typebot,
|
Typebot,
|
||||||
'settings' | 'theme' | 'id' | 'name' | 'isClosed' | 'isArchived'
|
'settings' | 'theme' | 'name' | 'isClosed' | 'isArchived' | 'publicId'
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasInitializedChat = false
|
|
||||||
|
|
||||||
export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
|
export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
|
||||||
const { asPath, push } = useRouter()
|
const { asPath, push } = useRouter()
|
||||||
const [initialChatReply, setInitialChatReply] = useState<InitialChatReply>()
|
|
||||||
const [error, setError] = useState<Error | undefined>(undefined)
|
|
||||||
|
|
||||||
const background = typebot.theme.general.background
|
const background = typebot.theme.general.background
|
||||||
|
|
||||||
const clearQueryParamsIfNecessary = useCallback(() => {
|
const clearQueryParamsIfNecessary = () => {
|
||||||
const hasQueryParams = asPath.includes('?')
|
const hasQueryParams = asPath.includes('?')
|
||||||
if (
|
if (
|
||||||
!hasQueryParams ||
|
!hasQueryParams ||
|
||||||
@ -35,44 +24,8 @@ export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
push(asPath.split('?')[0], undefined, { shallow: true })
|
push(asPath.split('?')[0], undefined, { shallow: true })
|
||||||
}, [asPath, push, typebot.settings.general.isHideQueryParamsEnabled])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(open)
|
|
||||||
clearQueryParamsIfNecessary()
|
|
||||||
}, [clearQueryParamsIfNecessary])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasInitializedChat) return
|
|
||||||
hasInitializedChat = true
|
|
||||||
const prefilledVariables = extractPrefilledVariables()
|
|
||||||
const existingResultId = getExistingResultFromSession() ?? undefined
|
|
||||||
|
|
||||||
getInitialChatReplyQuery({
|
|
||||||
typebotId: typebot.id,
|
|
||||||
resultId:
|
|
||||||
typebot.settings.general.isNewResultOnRefreshEnabled ?? false
|
|
||||||
? undefined
|
|
||||||
: existingResultId,
|
|
||||||
prefilledVariables,
|
|
||||||
}).then(({ data, error }) => {
|
|
||||||
if (error && 'code' in error && error.code === 'FORBIDDEN') {
|
|
||||||
setError(new Error('This bot is now closed.'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!data) return setError(new Error("Couldn't initiate the chat"))
|
|
||||||
setInitialChatReply(data)
|
|
||||||
setResultInSession(data.resultId)
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
initialChatReply,
|
|
||||||
typebot.id,
|
|
||||||
typebot.settings.general.isNewResultOnRefreshEnabled,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorPage error={error} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -89,20 +42,12 @@ export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
|
|||||||
typebotName={typebot.name}
|
typebotName={typebot.name}
|
||||||
metadata={typebot.settings.metadata}
|
metadata={typebot.settings.metadata}
|
||||||
/>
|
/>
|
||||||
{initialChatReply && (
|
{typebot.publicId && (
|
||||||
<Standard typebotId={typebot.id} initialChatReply={initialChatReply} />
|
<Standard
|
||||||
|
typebot={typebot.publicId}
|
||||||
|
onInit={clearQueryParamsIfNecessary}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractPrefilledVariables = () => {
|
|
||||||
const urlParams = new URLSearchParams(location.search)
|
|
||||||
|
|
||||||
const prefilledVariables: { [key: string]: string } = {}
|
|
||||||
urlParams.forEach((value, key) => {
|
|
||||||
prefilledVariables[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return prefilledVariables
|
|
||||||
}
|
|
||||||
|
@ -3,4 +3,9 @@ import { ChoiceInputBlock } from 'models'
|
|||||||
export const validateButtonInput = (
|
export const validateButtonInput = (
|
||||||
buttonBlock: ChoiceInputBlock,
|
buttonBlock: ChoiceInputBlock,
|
||||||
input: string
|
input: string
|
||||||
) => buttonBlock.items.some((item) => item.content === input)
|
) =>
|
||||||
|
input
|
||||||
|
.split(',')
|
||||||
|
.every((value) =>
|
||||||
|
buttonBlock.items.some((item) => item.content === value.trim())
|
||||||
|
)
|
||||||
|
@ -2,6 +2,7 @@ import { checkChatsUsage } from '@/features/usage'
|
|||||||
import {
|
import {
|
||||||
parsePrefilledVariables,
|
parsePrefilledVariables,
|
||||||
deepParseVariable,
|
deepParseVariable,
|
||||||
|
parseVariables,
|
||||||
} from '@/features/variables'
|
} from '@/features/variables'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { publicProcedure } from '@/utils/server/trpc'
|
import { publicProcedure } from '@/utils/server/trpc'
|
||||||
@ -11,15 +12,17 @@ import {
|
|||||||
ChatReply,
|
ChatReply,
|
||||||
chatReplySchema,
|
chatReplySchema,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
PublicTypebot,
|
|
||||||
Result,
|
Result,
|
||||||
sendMessageInputSchema,
|
sendMessageInputSchema,
|
||||||
SessionState,
|
SessionState,
|
||||||
StartParams,
|
StartParams,
|
||||||
|
StartTypebot,
|
||||||
|
Theme,
|
||||||
Typebot,
|
Typebot,
|
||||||
Variable,
|
Variable,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { continueBotFlow, getSession, startBotFlow } from '../utils'
|
import { continueBotFlow, getSession, startBotFlow } from '../utils'
|
||||||
|
import { omit } from 'utils'
|
||||||
|
|
||||||
export const sendMessageProcedure = publicProcedure
|
export const sendMessageProcedure = publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -37,12 +40,13 @@ export const sendMessageProcedure = publicProcedure
|
|||||||
const session = sessionId ? await getSession(sessionId) : null
|
const session = sessionId ? await getSession(sessionId) : null
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const { sessionId, typebot, messages, input, resultId } =
|
const { sessionId, typebot, messages, input, resultId, dynamicTheme } =
|
||||||
await startSession(startParams)
|
await startSession(startParams)
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
typebot: typebot
|
typebot: typebot
|
||||||
? {
|
? {
|
||||||
|
id: typebot.id,
|
||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
}
|
}
|
||||||
@ -50,6 +54,7 @@ export const sendMessageProcedure = publicProcedure
|
|||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
resultId,
|
resultId,
|
||||||
|
dynamicTheme,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { messages, input, logic, newSessionState, integrations } =
|
const { messages, input, logic, newSessionState, integrations } =
|
||||||
@ -67,89 +72,35 @@ export const sendMessageProcedure = publicProcedure
|
|||||||
input,
|
input,
|
||||||
logic,
|
logic,
|
||||||
integrations,
|
integrations,
|
||||||
|
dynamicTheme: parseDynamicThemeReply(newSessionState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const startSession = async (startParams?: StartParams) => {
|
const startSession = async (startParams?: StartParams) => {
|
||||||
if (!startParams?.typebotId)
|
if (!startParams?.typebot)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message: 'No typebotId provided in startParams',
|
message: 'No typebot provided in startParams',
|
||||||
})
|
|
||||||
const typebotQuery = startParams.isPreview
|
|
||||||
? await prisma.typebot.findUnique({
|
|
||||||
where: { id: startParams.typebotId },
|
|
||||||
select: {
|
|
||||||
groups: true,
|
|
||||||
edges: true,
|
|
||||||
settings: true,
|
|
||||||
theme: true,
|
|
||||||
variables: true,
|
|
||||||
isArchived: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: await prisma.typebot.findUnique({
|
|
||||||
where: { id: startParams.typebotId },
|
|
||||||
select: {
|
|
||||||
publishedTypebot: {
|
|
||||||
select: {
|
|
||||||
groups: true,
|
|
||||||
edges: true,
|
|
||||||
settings: true,
|
|
||||||
theme: true,
|
|
||||||
variables: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: true,
|
|
||||||
isClosed: true,
|
|
||||||
isArchived: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const typebot =
|
|
||||||
typebotQuery && 'publishedTypebot' in typebotQuery
|
|
||||||
? (typebotQuery.publishedTypebot as Pick<
|
|
||||||
PublicTypebot,
|
|
||||||
'groups' | 'edges' | 'settings' | 'theme' | 'variables'
|
|
||||||
>)
|
|
||||||
: (typebotQuery as Pick<
|
|
||||||
Typebot,
|
|
||||||
'groups' | 'edges' | 'settings' | 'theme' | 'variables' | 'isArchived'
|
|
||||||
>)
|
|
||||||
|
|
||||||
if (!typebot)
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
message: 'Typebot not found',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('isClosed' in typebot && typebot.isClosed)
|
const typebot = await getTypebot(startParams)
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'Typebot is closed',
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasReachedLimit = !startParams.isPreview
|
|
||||||
? await checkChatsUsage(startParams.typebotId)
|
|
||||||
: false
|
|
||||||
|
|
||||||
if (hasReachedLimit)
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'FORBIDDEN',
|
|
||||||
message: 'Your workspace reached its chat limit',
|
|
||||||
})
|
|
||||||
|
|
||||||
const startVariables = startParams.prefilledVariables
|
const startVariables = startParams.prefilledVariables
|
||||||
? parsePrefilledVariables(typebot.variables, startParams.prefilledVariables)
|
? parsePrefilledVariables(typebot.variables, startParams.prefilledVariables)
|
||||||
: typebot.variables
|
: typebot.variables
|
||||||
|
|
||||||
const result = await getResult({ ...startParams, startVariables })
|
const result = await getResult({
|
||||||
|
...startParams,
|
||||||
|
typebot: typebot.id,
|
||||||
|
startVariables,
|
||||||
|
isNewResultOnRefreshEnabled:
|
||||||
|
typebot.settings.general.isNewResultOnRefreshEnabled ?? false,
|
||||||
|
})
|
||||||
|
|
||||||
const initialState: SessionState = {
|
const initialState: SessionState = {
|
||||||
typebot: {
|
typebot: {
|
||||||
id: startParams.typebotId,
|
id: typebot.id,
|
||||||
groups: typebot.groups,
|
groups: typebot.groups,
|
||||||
edges: typebot.edges,
|
edges: typebot.edges,
|
||||||
variables: startVariables,
|
variables: startVariables,
|
||||||
@ -161,8 +112,9 @@ const startSession = async (startParams?: StartParams) => {
|
|||||||
result: result
|
result: result
|
||||||
? { id: result.id, variables: result.variables, hasStarted: false }
|
? { id: result.id, variables: result.variables, hasStarted: false }
|
||||||
: undefined,
|
: undefined,
|
||||||
isPreview: false,
|
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
|
||||||
currentTypebotId: startParams.typebotId,
|
currentTypebotId: typebot.id,
|
||||||
|
dynamicTheme: parseDynamicThemeInState(typebot.theme),
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -170,16 +122,26 @@ const startSession = async (startParams?: StartParams) => {
|
|||||||
input,
|
input,
|
||||||
logic,
|
logic,
|
||||||
newSessionState: newInitialState,
|
newSessionState: newInitialState,
|
||||||
} = await startBotFlow(initialState)
|
} = await startBotFlow(initialState, startParams.startGroupId)
|
||||||
|
|
||||||
if (!input)
|
if (!input)
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
logic,
|
logic,
|
||||||
|
typebot: {
|
||||||
|
id: typebot.id,
|
||||||
|
settings: deepParseVariable(newInitialState.typebot.variables)(
|
||||||
|
typebot.settings
|
||||||
|
),
|
||||||
|
theme: deepParseVariable(newInitialState.typebot.variables)(
|
||||||
|
typebot.theme
|
||||||
|
),
|
||||||
|
},
|
||||||
|
dynamicTheme: parseDynamicThemeReply(newInitialState),
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionState: ChatSession['state'] = {
|
const sessionState: ChatSession['state'] = {
|
||||||
...(newInitialState ?? initialState),
|
...newInitialState,
|
||||||
currentBlock: {
|
currentBlock: {
|
||||||
groupId: input.groupId,
|
groupId: input.groupId,
|
||||||
blockId: input.id,
|
blockId: input.id,
|
||||||
@ -196,27 +158,122 @@ const startSession = async (startParams?: StartParams) => {
|
|||||||
resultId: result?.id,
|
resultId: result?.id,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
typebot: {
|
typebot: {
|
||||||
settings: deepParseVariable(typebot.variables)(typebot.settings),
|
id: typebot.id,
|
||||||
theme: deepParseVariable(typebot.variables)(typebot.theme),
|
settings: deepParseVariable(newInitialState.typebot.variables)(
|
||||||
|
typebot.settings
|
||||||
|
),
|
||||||
|
theme: deepParseVariable(newInitialState.typebot.variables)(
|
||||||
|
typebot.theme
|
||||||
|
),
|
||||||
},
|
},
|
||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
logic,
|
logic,
|
||||||
|
dynamicTheme: parseDynamicThemeReply(newInitialState),
|
||||||
} satisfies ChatReply
|
} satisfies ChatReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTypebot = async ({
|
||||||
|
typebot,
|
||||||
|
isPreview,
|
||||||
|
}: Pick<StartParams, 'typebot' | 'isPreview'>): Promise<StartTypebot> => {
|
||||||
|
if (typeof typebot !== 'string') return typebot
|
||||||
|
const typebotQuery = isPreview
|
||||||
|
? await prisma.typebot.findUnique({
|
||||||
|
where: { id: typebot },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
groups: true,
|
||||||
|
edges: true,
|
||||||
|
settings: true,
|
||||||
|
theme: true,
|
||||||
|
variables: true,
|
||||||
|
isArchived: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: await prisma.publicTypebot.findFirst({
|
||||||
|
where: { typebot: { publicId: typebot } },
|
||||||
|
select: {
|
||||||
|
groups: true,
|
||||||
|
edges: true,
|
||||||
|
settings: true,
|
||||||
|
theme: true,
|
||||||
|
variables: true,
|
||||||
|
typebotId: true,
|
||||||
|
typebot: {
|
||||||
|
select: {
|
||||||
|
isArchived: true,
|
||||||
|
isClosed: true,
|
||||||
|
workspace: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
plan: true,
|
||||||
|
additionalChatsIndex: true,
|
||||||
|
chatsLimitFirstEmailSentAt: true,
|
||||||
|
chatsLimitSecondEmailSentAt: true,
|
||||||
|
customChatsLimit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedTypebot =
|
||||||
|
typebotQuery && 'typebot' in typebotQuery
|
||||||
|
? ({
|
||||||
|
id: typebotQuery.typebotId,
|
||||||
|
...omit(typebotQuery.typebot, 'workspace'),
|
||||||
|
...omit(typebotQuery, 'typebot', 'typebotId'),
|
||||||
|
} as StartTypebot & Pick<Typebot, 'isArchived' | 'isClosed'>)
|
||||||
|
: (typebotQuery as StartTypebot & Pick<Typebot, 'isArchived'>)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!parsedTypebot ||
|
||||||
|
('isArchived' in parsedTypebot && parsedTypebot.isArchived)
|
||||||
|
)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Typebot not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('isClosed' in parsedTypebot && parsedTypebot.isClosed)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Typebot is closed',
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasReachedLimit =
|
||||||
|
typebotQuery && 'typebot' in typebotQuery
|
||||||
|
? await checkChatsUsage({
|
||||||
|
typebotId: parsedTypebot.id,
|
||||||
|
workspace: typebotQuery.typebot.workspace,
|
||||||
|
})
|
||||||
|
: false
|
||||||
|
|
||||||
|
if (hasReachedLimit)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You have reached your chats limit',
|
||||||
|
})
|
||||||
|
|
||||||
|
return parsedTypebot
|
||||||
|
}
|
||||||
|
|
||||||
const getResult = async ({
|
const getResult = async ({
|
||||||
typebotId,
|
typebot,
|
||||||
isPreview,
|
isPreview,
|
||||||
resultId,
|
resultId,
|
||||||
startVariables,
|
startVariables,
|
||||||
}: Pick<StartParams, 'isPreview' | 'resultId' | 'typebotId'> & {
|
isNewResultOnRefreshEnabled,
|
||||||
|
}: Pick<StartParams, 'isPreview' | 'resultId' | 'typebot'> & {
|
||||||
startVariables: Variable[]
|
startVariables: Variable[]
|
||||||
|
isNewResultOnRefreshEnabled: boolean
|
||||||
}) => {
|
}) => {
|
||||||
if (isPreview) return undefined
|
if (isPreview || typeof typebot !== 'string') return undefined
|
||||||
const data = {
|
const data = {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
typebotId: typebotId,
|
typebotId: typebot,
|
||||||
variables: { set: startVariables.filter((variable) => variable.value) },
|
variables: { set: startVariables.filter((variable) => variable.value) },
|
||||||
} satisfies Prisma.ResultUncheckedCreateInput
|
} satisfies Prisma.ResultUncheckedCreateInput
|
||||||
const select = {
|
const select = {
|
||||||
@ -225,7 +282,7 @@ const getResult = async ({
|
|||||||
hasStarted: true,
|
hasStarted: true,
|
||||||
} satisfies Prisma.ResultSelect
|
} satisfies Prisma.ResultSelect
|
||||||
return (
|
return (
|
||||||
resultId
|
resultId && !isNewResultOnRefreshEnabled
|
||||||
? await prisma.result.update({
|
? await prisma.result.update({
|
||||||
where: { id: resultId },
|
where: { id: resultId },
|
||||||
data,
|
data,
|
||||||
@ -237,3 +294,36 @@ const getResult = async ({
|
|||||||
})
|
})
|
||||||
) as Pick<Result, 'id' | 'variables' | 'hasStarted'>
|
) as Pick<Result, 'id' | 'variables' | 'hasStarted'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseDynamicThemeInState = (theme: Theme) => {
|
||||||
|
const hostAvatarUrl =
|
||||||
|
theme.chat.hostAvatar?.isEnabled ?? true
|
||||||
|
? theme.chat.hostAvatar?.url
|
||||||
|
: undefined
|
||||||
|
const guestAvatarUrl =
|
||||||
|
theme.chat.guestAvatar?.isEnabled ?? false
|
||||||
|
? theme.chat.guestAvatar?.url
|
||||||
|
: undefined
|
||||||
|
if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{'))
|
||||||
|
return
|
||||||
|
return {
|
||||||
|
hostAvatarUrl: hostAvatarUrl?.startsWith('{{') ? hostAvatarUrl : undefined,
|
||||||
|
guestAvatarUrl: guestAvatarUrl?.startsWith('{{')
|
||||||
|
? guestAvatarUrl
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseDynamicThemeReply = (
|
||||||
|
state: SessionState | undefined
|
||||||
|
): ChatReply['dynamicTheme'] => {
|
||||||
|
if (!state?.dynamicTheme) return
|
||||||
|
return {
|
||||||
|
hostAvatarUrl: parseVariables(state?.typebot.variables)(
|
||||||
|
state.dynamicTheme.hostAvatarUrl
|
||||||
|
),
|
||||||
|
guestAvatarUrl: parseVariables(state?.typebot.variables)(
|
||||||
|
state.dynamicTheme.guestAvatarUrl
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,7 +23,7 @@ export const executeGroup =
|
|||||||
(state: SessionState, currentReply?: ChatReply) =>
|
(state: SessionState, currentReply?: ChatReply) =>
|
||||||
async (
|
async (
|
||||||
group: Group
|
group: Group
|
||||||
): Promise<ChatReply & { newSessionState?: SessionState }> => {
|
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||||
const messages: ChatReply['messages'] = currentReply?.messages ?? []
|
const messages: ChatReply['messages'] = currentReply?.messages ?? []
|
||||||
let logic: ChatReply['logic'] = currentReply?.logic
|
let logic: ChatReply['logic'] = currentReply?.logic
|
||||||
let integrations: ChatReply['integrations'] = currentReply?.integrations
|
let integrations: ChatReply['integrations'] = currentReply?.integrations
|
||||||
@ -72,8 +72,10 @@ export const executeGroup =
|
|||||||
integrations = executionResponse.integrations
|
integrations = executionResponse.integrations
|
||||||
if (executionResponse.newSessionState)
|
if (executionResponse.newSessionState)
|
||||||
newSessionState = executionResponse.newSessionState
|
newSessionState = executionResponse.newSessionState
|
||||||
if (executionResponse.outgoingEdgeId)
|
if (executionResponse.outgoingEdgeId) {
|
||||||
nextEdgeId = executionResponse.outgoingEdgeId
|
nextEdgeId = executionResponse.outgoingEdgeId
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextEdgeId) return { messages, newSessionState, logic, integrations }
|
if (!nextEdgeId) return { messages, newSessionState, logic, integrations }
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
import { ChatReply, SessionState } from 'models'
|
import { ChatReply, SessionState } from 'models'
|
||||||
import { executeGroup } from './executeGroup'
|
import { executeGroup } from './executeGroup'
|
||||||
import { getNextGroup } from './getNextGroup'
|
import { getNextGroup } from './getNextGroup'
|
||||||
|
|
||||||
export const startBotFlow = async (
|
export const startBotFlow = async (
|
||||||
state: SessionState
|
state: SessionState,
|
||||||
): Promise<ChatReply & { newSessionState?: SessionState }> => {
|
startGroupId?: string
|
||||||
|
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||||
|
if (startGroupId) {
|
||||||
|
const group = state.typebot.groups.find(
|
||||||
|
(group) => group.id === startGroupId
|
||||||
|
)
|
||||||
|
if (!group)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: "startGroupId doesn't exist",
|
||||||
|
})
|
||||||
|
return executeGroup(state)(group)
|
||||||
|
}
|
||||||
const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId
|
const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId
|
||||||
if (!firstEdgeId) return { messages: [] }
|
if (!firstEdgeId) return { messages: [], newSessionState: state }
|
||||||
const nextGroup = getNextGroup(state)(firstEdgeId)
|
const nextGroup = getNextGroup(state)(firstEdgeId)
|
||||||
if (!nextGroup) return { messages: [] }
|
if (!nextGroup) return { messages: [], newSessionState: state }
|
||||||
return executeGroup(state)(nextGroup.group)
|
return executeGroup(state)(nextGroup.group)
|
||||||
}
|
}
|
||||||
|
@ -37,11 +37,10 @@ test('API chat execution should work on preview bot', async ({ request }) => {
|
|||||||
await request.post(`/api/v1/sendMessage`, {
|
await request.post(`/api/v1/sendMessage`, {
|
||||||
data: {
|
data: {
|
||||||
startParams: {
|
startParams: {
|
||||||
typebotId,
|
typebot: typebotId,
|
||||||
isPreview: true,
|
isPreview: true,
|
||||||
},
|
},
|
||||||
// TODO: replace with satisfies once compatible with playwright
|
} satisfies SendMessageInput,
|
||||||
} as SendMessageInput,
|
|
||||||
})
|
})
|
||||||
).json()
|
).json()
|
||||||
expect(resultId).toBeUndefined()
|
expect(resultId).toBeUndefined()
|
||||||
@ -75,10 +74,9 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
|||||||
await request.post(`/api/v1/sendMessage`, {
|
await request.post(`/api/v1/sendMessage`, {
|
||||||
data: {
|
data: {
|
||||||
startParams: {
|
startParams: {
|
||||||
typebotId,
|
typebot: publicId,
|
||||||
},
|
},
|
||||||
// TODO: replace with satisfies once compatible with playwright
|
} satisfies SendMessageInput,
|
||||||
} as SendMessageInput,
|
|
||||||
})
|
})
|
||||||
).json()
|
).json()
|
||||||
chatSessionId = sessionId
|
chatSessionId = sessionId
|
||||||
|
@ -27,8 +27,8 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
|
|||||||
])
|
])
|
||||||
const { resultId } = await response.json()
|
const { resultId } = await response.json()
|
||||||
expect(resultId).toBeDefined()
|
expect(resultId).toBeDefined()
|
||||||
|
|
||||||
await expect(page.getByRole('textbox')).toBeVisible()
|
await expect(page.getByRole('textbox')).toBeVisible()
|
||||||
|
|
||||||
const [, secondResponse] = await Promise.all([
|
const [, secondResponse] = await Promise.all([
|
||||||
page.reload(),
|
page.reload(),
|
||||||
page.waitForResponse(/sendMessage/),
|
page.waitForResponse(/sendMessage/),
|
||||||
|
@ -4,30 +4,44 @@ import {
|
|||||||
sendAlmostReachedChatsLimitEmail,
|
sendAlmostReachedChatsLimitEmail,
|
||||||
sendReachedChatsLimitEmail,
|
sendReachedChatsLimitEmail,
|
||||||
} from 'emails'
|
} from 'emails'
|
||||||
|
import { Workspace } from 'models'
|
||||||
import { env, getChatsLimit, isDefined } from 'utils'
|
import { env, getChatsLimit, isDefined } from 'utils'
|
||||||
|
|
||||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||||
|
|
||||||
export const checkChatsUsage = async (typebotId: string) => {
|
export const checkChatsUsage = async (props: {
|
||||||
const typebot = await prisma.typebot.findUnique({
|
typebotId: string
|
||||||
where: {
|
workspace?: Pick<
|
||||||
id: typebotId,
|
Workspace,
|
||||||
},
|
| 'id'
|
||||||
include: {
|
| 'plan'
|
||||||
workspace: {
|
| 'additionalChatsIndex'
|
||||||
select: {
|
| 'chatsLimitFirstEmailSentAt'
|
||||||
id: true,
|
| 'chatsLimitSecondEmailSentAt'
|
||||||
plan: true,
|
| 'customChatsLimit'
|
||||||
additionalChatsIndex: true,
|
>
|
||||||
chatsLimitFirstEmailSentAt: true,
|
}) => {
|
||||||
chatsLimitSecondEmailSentAt: true,
|
const typebot = props.workspace
|
||||||
customChatsLimit: true,
|
? null
|
||||||
|
: await prisma.typebot.findUnique({
|
||||||
|
where: {
|
||||||
|
id: props.typebotId,
|
||||||
},
|
},
|
||||||
},
|
include: {
|
||||||
},
|
workspace: {
|
||||||
})
|
select: {
|
||||||
|
id: true,
|
||||||
|
plan: true,
|
||||||
|
additionalChatsIndex: true,
|
||||||
|
chatsLimitFirstEmailSentAt: true,
|
||||||
|
chatsLimitSecondEmailSentAt: true,
|
||||||
|
customChatsLimit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const workspace = typebot?.workspace
|
const workspace = props.workspace || typebot?.workspace
|
||||||
|
|
||||||
if (!workspace) return false
|
if (!workspace) return false
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const typebotId = req.query.typebotId as string
|
const typebotId = req.query.typebotId as string
|
||||||
const hasReachedLimit = await checkChatsUsage(typebotId)
|
const hasReachedLimit = await checkChatsUsage({ typebotId })
|
||||||
if (hasReachedLimit) return res.send({ result: null, hasReachedLimit })
|
if (hasReachedLimit) return res.send({ result: null, hasReachedLimit })
|
||||||
const result = await prisma.result.create({
|
const result = await prisma.result.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -56,12 +56,12 @@ const getTypebotFromPublicId = async (
|
|||||||
const typebot = (await prisma.typebot.findUnique({
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
where: { publicId },
|
where: { publicId },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
|
||||||
theme: true,
|
theme: true,
|
||||||
name: true,
|
name: true,
|
||||||
settings: true,
|
settings: true,
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
isClosed: true,
|
isClosed: true,
|
||||||
|
publicId: true,
|
||||||
},
|
},
|
||||||
})) as TypebotPageV2Props['typebot'] | null
|
})) as TypebotPageV2Props['typebot'] | null
|
||||||
if (isNotDefined(typebot)) return null
|
if (isNotDefined(typebot)) return null
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { InitialChatReply, SendMessageInput } from 'models'
|
|
||||||
import { sendRequest } from 'utils'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
typebotId: string
|
|
||||||
resultId?: string
|
|
||||||
prefilledVariables?: Record<string, string>
|
|
||||||
}
|
|
||||||
export async function getInitialChatReplyQuery({
|
|
||||||
typebotId,
|
|
||||||
resultId,
|
|
||||||
prefilledVariables,
|
|
||||||
}: Props) {
|
|
||||||
if (!typebotId)
|
|
||||||
throw new Error('Typebot ID is required to get initial messages')
|
|
||||||
|
|
||||||
return sendRequest<InitialChatReply>({
|
|
||||||
method: 'POST',
|
|
||||||
url: `/api/v1/sendMessage`,
|
|
||||||
body: {
|
|
||||||
startParams: {
|
|
||||||
typebotId,
|
|
||||||
resultId,
|
|
||||||
prefilledVariables,
|
|
||||||
},
|
|
||||||
} satisfies SendMessageInput,
|
|
||||||
})
|
|
||||||
}
|
|
@ -20,7 +20,6 @@ export const parseReadableDate = ({
|
|||||||
const fromReadable = new Date(
|
const fromReadable = new Date(
|
||||||
hasTime ? from : from.replace(/-/g, '/')
|
hasTime ? from : from.replace(/-/g, '/')
|
||||||
).toLocaleString(currentLocale, formatOptions)
|
).toLocaleString(currentLocale, formatOptions)
|
||||||
console.log(to, to.replace(/-/g, '/'))
|
|
||||||
const toReadable = new Date(
|
const toReadable = new Date(
|
||||||
hasTime ? to : to.replace(/-/g, '/')
|
hasTime ? to : to.replace(/-/g, '/')
|
||||||
).toLocaleString(currentLocale, formatOptions)
|
).toLocaleString(currentLocale, formatOptions)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"main": "dist/index.mjs",
|
"main": "dist/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:host,
|
:host {
|
||||||
:root {
|
|
||||||
--typebot-container-bg-image: none;
|
--typebot-container-bg-image: none;
|
||||||
--typebot-container-bg-color: transparent;
|
--typebot-container-bg-color: transparent;
|
||||||
--typebot-container-font-family: 'Open Sans';
|
--typebot-container-font-family: 'Open Sans';
|
||||||
|
@ -1,83 +1,128 @@
|
|||||||
import { LiteBadge } from './LiteBadge'
|
import { LiteBadge } from './LiteBadge'
|
||||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import {
|
import { getViewerUrl, injectCustomHeadCode, isEmpty, isNotEmpty } from 'utils'
|
||||||
getViewerUrl,
|
|
||||||
injectCustomHeadCode,
|
|
||||||
isDefined,
|
|
||||||
isNotEmpty,
|
|
||||||
} from 'utils'
|
|
||||||
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
||||||
import { ConversationContainer } from './ConversationContainer'
|
import { ConversationContainer } from './ConversationContainer'
|
||||||
import css from '../assets/index.css'
|
import css from '../assets/index.css'
|
||||||
import { InitialChatReply, StartParams } from 'models'
|
import { StartParams } from 'models'
|
||||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||||
import { BotContext } from '@/types'
|
import { BotContext, InitialChatReply } from '@/types'
|
||||||
|
import { ErrorMessage } from './ErrorMessage'
|
||||||
|
import {
|
||||||
|
getExistingResultIdFromSession,
|
||||||
|
setResultInSession,
|
||||||
|
} from '@/utils/sessionStorage'
|
||||||
|
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||||
|
|
||||||
export type BotProps = StartParams & {
|
export type BotProps = StartParams & {
|
||||||
initialChatReply?: InitialChatReply
|
|
||||||
apiHost?: string
|
apiHost?: string
|
||||||
|
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||||
|
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||||
|
onInit?: () => void
|
||||||
|
onEnd?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Bot = (props: BotProps) => {
|
export const Bot = (props: BotProps) => {
|
||||||
const [initialChatReply, setInitialChatReply] = createSignal<
|
const [initialChatReply, setInitialChatReply] = createSignal<
|
||||||
InitialChatReply | undefined
|
InitialChatReply | undefined
|
||||||
>(props.initialChatReply)
|
>()
|
||||||
|
const [error, setError] = createSignal<Error | undefined>(
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
isEmpty(isEmpty(props.apiHost) ? getViewerUrl() : props.apiHost)
|
||||||
|
? new Error('process.env.NEXT_PUBLIC_VIEWER_URL is missing in env')
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
const initializeBot = async () => {
|
||||||
|
const urlParams = new URLSearchParams(location.search)
|
||||||
|
props.onInit?.()
|
||||||
|
const prefilledVariables: { [key: string]: string } = {}
|
||||||
|
urlParams.forEach((value, key) => {
|
||||||
|
prefilledVariables[key] = value
|
||||||
|
})
|
||||||
|
const { data, error } = await getInitialChatReplyQuery({
|
||||||
|
typebot: props.typebot,
|
||||||
|
apiHost: props.apiHost,
|
||||||
|
isPreview: props.isPreview ?? false,
|
||||||
|
resultId: isNotEmpty(props.resultId)
|
||||||
|
? props.resultId
|
||||||
|
: getExistingResultIdFromSession(),
|
||||||
|
startGroupId: props.startGroupId,
|
||||||
|
prefilledVariables: {
|
||||||
|
...prefilledVariables,
|
||||||
|
...props.prefilledVariables,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (error && 'code' in error && typeof error.code === 'string') {
|
||||||
|
if (['BAD_REQUEST', 'FORBIDDEN'].includes(error.code))
|
||||||
|
setError(new Error('This bot is now closed.'))
|
||||||
|
if (error.code === 'NOT_FOUND') setError(new Error('Typebot not found.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) return setError(new Error("Couldn't initiate the chat"))
|
||||||
|
|
||||||
|
if (data.resultId) setResultInSession(data.resultId)
|
||||||
|
setInitialChatReply(data)
|
||||||
|
|
||||||
|
if (data.input?.id && props.onNewInputBlock)
|
||||||
|
props.onNewInputBlock({
|
||||||
|
id: data.input.id,
|
||||||
|
groupId: data.input.groupId,
|
||||||
|
})
|
||||||
|
const customHeadCode = data.typebot.settings.metadata.customHeadCode
|
||||||
|
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!props.typebotId) return
|
initializeBot().then()
|
||||||
const initialChatReplyValue = initialChatReply()
|
|
||||||
if (isDefined(initialChatReplyValue)) {
|
|
||||||
const customHeadCode =
|
|
||||||
initialChatReplyValue.typebot.settings.metadata.customHeadCode
|
|
||||||
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
|
||||||
} else {
|
|
||||||
const urlParams = new URLSearchParams(location.search)
|
|
||||||
const prefilledVariables: { [key: string]: string } = {}
|
|
||||||
urlParams.forEach((value, key) => {
|
|
||||||
prefilledVariables[key] = value
|
|
||||||
})
|
|
||||||
getInitialChatReplyQuery({
|
|
||||||
typebotId: props.typebotId,
|
|
||||||
apiHost: props.apiHost,
|
|
||||||
isPreview: props.isPreview ?? false,
|
|
||||||
resultId: props.resultId,
|
|
||||||
prefilledVariables: {
|
|
||||||
...prefilledVariables,
|
|
||||||
...props.prefilledVariables,
|
|
||||||
},
|
|
||||||
}).then((initialChatReply) => {
|
|
||||||
setInitialChatReply(initialChatReply)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<>
|
||||||
when={isNotEmpty(props.apiHost ?? getViewerUrl())}
|
<style>{css}</style>
|
||||||
fallback={() => (
|
<Show when={error()} keyed>
|
||||||
<p>process.env.NEXT_PUBLIC_VIEWER_URL is missing in env</p>
|
{(error) => <ErrorMessage error={error} />}
|
||||||
)}
|
</Show>
|
||||||
>
|
|
||||||
<Show when={initialChatReply()} keyed>
|
<Show when={initialChatReply()} keyed>
|
||||||
{(initialChatReply) => (
|
{(initialChatReply) => (
|
||||||
<BotContent
|
<BotContent
|
||||||
initialChatReply={initialChatReply}
|
initialChatReply={{
|
||||||
|
...initialChatReply,
|
||||||
|
typebot: {
|
||||||
|
...initialChatReply.typebot,
|
||||||
|
settings:
|
||||||
|
typeof props.typebot === 'string'
|
||||||
|
? initialChatReply.typebot?.settings
|
||||||
|
: props.typebot?.settings,
|
||||||
|
theme:
|
||||||
|
typeof props.typebot === 'string'
|
||||||
|
? initialChatReply.typebot?.theme
|
||||||
|
: props.typebot?.theme,
|
||||||
|
},
|
||||||
|
}}
|
||||||
context={{
|
context={{
|
||||||
apiHost: props.apiHost,
|
apiHost: props.apiHost,
|
||||||
isPreview: props.isPreview ?? false,
|
isPreview: props.isPreview ?? false,
|
||||||
typebotId: props.typebotId as string,
|
typebotId: initialChatReply.typebot.id,
|
||||||
resultId: initialChatReply.resultId,
|
resultId: initialChatReply.resultId,
|
||||||
}}
|
}}
|
||||||
|
onNewInputBlock={props.onNewInputBlock}
|
||||||
|
onAnswer={props.onAnswer}
|
||||||
|
onEnd={props.onEnd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotContentProps = {
|
type BotContentProps = {
|
||||||
initialChatReply: InitialChatReply
|
initialChatReply: InitialChatReply
|
||||||
context: BotContext
|
context: BotContext
|
||||||
|
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||||
|
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||||
|
onEnd?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const BotContent = (props: BotContentProps) => {
|
const BotContent = (props: BotContentProps) => {
|
||||||
@ -98,38 +143,39 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
injectCustomFont()
|
injectCustomFont()
|
||||||
if (botContainer) {
|
if (!botContainer) return
|
||||||
resizeObserver.observe(botContainer)
|
resizeObserver.observe(botContainer)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!botContainer) return
|
||||||
|
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (botContainer) {
|
if (!botContainer) return
|
||||||
resizeObserver.unobserve(botContainer)
|
resizeObserver.unobserve(botContainer)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<style>{css}</style>
|
ref={botContainer}
|
||||||
<div
|
class="relative flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container"
|
||||||
ref={botContainer}
|
>
|
||||||
class="flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container"
|
<div class="flex w-full h-full justify-center">
|
||||||
>
|
<ConversationContainer
|
||||||
<div class="flex w-full h-full justify-center">
|
context={props.context}
|
||||||
<ConversationContainer
|
initialChatReply={props.initialChatReply}
|
||||||
context={props.context}
|
onNewInputBlock={props.onNewInputBlock}
|
||||||
initialChatReply={props.initialChatReply}
|
onAnswer={props.onAnswer}
|
||||||
/>
|
onEnd={props.onEnd}
|
||||||
</div>
|
/>
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
props.initialChatReply.typebot.settings.general.isBrandingEnabled
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LiteBadge botContainer={botContainer} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<Show
|
||||||
|
when={props.initialChatReply.typebot.settings.general.isBrandingEnabled}
|
||||||
|
>
|
||||||
|
<LiteBadge botContainer={botContainer} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
|
|||||||
settings: Settings
|
settings: Settings
|
||||||
inputIndex: number
|
inputIndex: number
|
||||||
context: BotContext
|
context: BotContext
|
||||||
|
onScrollToBottom: () => void
|
||||||
onSubmit: (input: string) => void
|
onSubmit: (input: string) => void
|
||||||
|
onEnd?: () => void
|
||||||
onSkip: () => void
|
onSkip: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +25,9 @@ export const ChatChunk = (props: Props) => {
|
|||||||
? displayedMessageIndex()
|
? displayedMessageIndex()
|
||||||
: displayedMessageIndex() + 1
|
: displayedMessageIndex() + 1
|
||||||
)
|
)
|
||||||
|
props.onScrollToBottom()
|
||||||
|
if (!props.input && displayedMessageIndex() === props.messages.length)
|
||||||
|
return props.onEnd?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,31 +1,73 @@
|
|||||||
import { ChatReply, InitialChatReply } from 'models'
|
import { ChatReply, Theme } from 'models'
|
||||||
import { createSignal, For } from 'solid-js'
|
import { createSignal, For } from 'solid-js'
|
||||||
import { sendMessageQuery } from '@/queries/sendMessageQuery'
|
import { sendMessageQuery } from '@/queries/sendMessageQuery'
|
||||||
import { ChatChunk } from './ChatChunk'
|
import { ChatChunk } from './ChatChunk'
|
||||||
import { BotContext } from '@/types'
|
import { BotContext, InitialChatReply } from '@/types'
|
||||||
import { executeIntegrations } from '@/utils/executeIntegrations'
|
import { executeIntegrations } from '@/utils/executeIntegrations'
|
||||||
import { executeLogic } from '@/utils/executeLogic'
|
import { executeLogic } from '@/utils/executeLogic'
|
||||||
|
|
||||||
|
const parseDynamicTheme = (
|
||||||
|
theme: Theme,
|
||||||
|
dynamicTheme: ChatReply['dynamicTheme']
|
||||||
|
): Theme => ({
|
||||||
|
...theme,
|
||||||
|
chat: {
|
||||||
|
...theme.chat,
|
||||||
|
hostAvatar: theme.chat.hostAvatar
|
||||||
|
? {
|
||||||
|
...theme.chat.hostAvatar,
|
||||||
|
url: dynamicTheme?.hostAvatarUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
guestAvatar: theme.chat.guestAvatar
|
||||||
|
? {
|
||||||
|
...theme.chat.guestAvatar,
|
||||||
|
url: dynamicTheme?.guestAvatarUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialChatReply: InitialChatReply
|
initialChatReply: InitialChatReply
|
||||||
context: BotContext
|
context: BotContext
|
||||||
|
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||||
|
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||||
|
onEnd?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConversationContainer = (props: Props) => {
|
export const ConversationContainer = (props: Props) => {
|
||||||
|
let bottomSpacer: HTMLDivElement | undefined
|
||||||
const [chatChunks, setChatChunks] = createSignal<ChatReply[]>([
|
const [chatChunks, setChatChunks] = createSignal<ChatReply[]>([
|
||||||
{
|
{
|
||||||
input: props.initialChatReply.input,
|
input: props.initialChatReply.input,
|
||||||
messages: props.initialChatReply.messages,
|
messages: props.initialChatReply.messages,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
const [theme, setTheme] = createSignal(
|
||||||
|
parseDynamicTheme(
|
||||||
|
props.initialChatReply.typebot.theme,
|
||||||
|
props.initialChatReply.dynamicTheme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const sendMessage = async (message: string) => {
|
const sendMessage = async (message: string) => {
|
||||||
|
const currentBlockId = chatChunks().at(-1)?.input?.id
|
||||||
|
if (currentBlockId && props.onAnswer)
|
||||||
|
props.onAnswer({ message, blockId: currentBlockId })
|
||||||
const data = await sendMessageQuery({
|
const data = await sendMessageQuery({
|
||||||
apiHost: props.context.apiHost,
|
apiHost: props.context.apiHost,
|
||||||
sessionId: props.initialChatReply.sessionId,
|
sessionId: props.initialChatReply.sessionId,
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
if (data.dynamicTheme) applyDynamicTheme(data.dynamicTheme)
|
||||||
|
if (data.input?.id && props.onNewInputBlock) {
|
||||||
|
props.onNewInputBlock({
|
||||||
|
id: data.input.id,
|
||||||
|
groupId: data.input.groupId,
|
||||||
|
})
|
||||||
|
}
|
||||||
if (data.integrations) {
|
if (data.integrations) {
|
||||||
executeIntegrations(data.integrations)
|
executeIntegrations(data.integrations)
|
||||||
}
|
}
|
||||||
@ -41,6 +83,17 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const applyDynamicTheme = (dynamicTheme: ChatReply['dynamicTheme']) => {
|
||||||
|
setTheme((theme) => parseDynamicTheme(theme, dynamicTheme))
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoScrollToBottom = () => {
|
||||||
|
if (!bottomSpacer) return
|
||||||
|
setTimeout(() => {
|
||||||
|
bottomSpacer?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="overflow-y-scroll w-full min-h-full rounded px-3 pt-10 relative scrollable-container typebot-chat-view">
|
<div class="overflow-y-scroll w-full min-h-full rounded px-3 pt-10 relative scrollable-container typebot-chat-view">
|
||||||
<For each={chatChunks()}>
|
<For each={chatChunks()}>
|
||||||
@ -49,16 +102,26 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
inputIndex={index()}
|
inputIndex={index()}
|
||||||
messages={chatChunk.messages}
|
messages={chatChunk.messages}
|
||||||
input={chatChunk.input}
|
input={chatChunk.input}
|
||||||
theme={props.initialChatReply.typebot.theme}
|
theme={theme()}
|
||||||
settings={props.initialChatReply.typebot.settings}
|
settings={props.initialChatReply.typebot.settings}
|
||||||
onSubmit={sendMessage}
|
onSubmit={sendMessage}
|
||||||
|
onScrollToBottom={autoScrollToBottom}
|
||||||
onSkip={() => {
|
onSkip={() => {
|
||||||
// TODO: implement skip
|
// TODO: implement skip
|
||||||
}}
|
}}
|
||||||
|
onEnd={props.onEnd}
|
||||||
context={props.context}
|
context={props.context}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
<BottomSpacer ref={bottomSpacer} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BottomSpacerProps = {
|
||||||
|
ref: HTMLDivElement | undefined
|
||||||
|
}
|
||||||
|
const BottomSpacer = (props: BottomSpacerProps) => {
|
||||||
|
return <div ref={props.ref} class="w-full h-32" />
|
||||||
|
}
|
||||||
|
10
packages/js/src/components/ErrorMessage.tsx
Normal file
10
packages/js/src/components/ErrorMessage.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
type Props = {
|
||||||
|
error: Error
|
||||||
|
}
|
||||||
|
export const ErrorMessage = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div class="h-full flex justify-center items-center flex-col">
|
||||||
|
<p class="text-5xl">{props.error.message}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -44,7 +44,7 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
|
|
||||||
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
||||||
setAnswer(label ?? value)
|
setAnswer(label ?? value)
|
||||||
props.onSubmit(value)
|
props.onSubmit(value ?? label)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -96,37 +96,40 @@ const Input = (props: {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.block.type === InputBlockType.TEXT}>
|
<Match when={props.block.type === InputBlockType.TEXT}>
|
||||||
<TextInput
|
<TextInput
|
||||||
block={props.block as TextInputBlock & { prefilledValue?: string }}
|
block={props.block as TextInputBlock}
|
||||||
|
defaultValue={props.block.prefilledValue}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
hasGuestAvatar={props.hasGuestAvatar}
|
hasGuestAvatar={props.hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.NUMBER}>
|
<Match when={props.block.type === InputBlockType.NUMBER}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
block={props.block as NumberInputBlock & { prefilledValue?: string }}
|
block={props.block as NumberInputBlock}
|
||||||
|
defaultValue={props.block.prefilledValue}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
hasGuestAvatar={props.hasGuestAvatar}
|
hasGuestAvatar={props.hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.EMAIL}>
|
<Match when={props.block.type === InputBlockType.EMAIL}>
|
||||||
<EmailInput
|
<EmailInput
|
||||||
block={props.block as EmailInputBlock & { prefilledValue?: string }}
|
block={props.block as EmailInputBlock}
|
||||||
|
defaultValue={props.block.prefilledValue}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
hasGuestAvatar={props.hasGuestAvatar}
|
hasGuestAvatar={props.hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.URL}>
|
<Match when={props.block.type === InputBlockType.URL}>
|
||||||
<UrlInput
|
<UrlInput
|
||||||
block={props.block as UrlInputBlock & { prefilledValue?: string }}
|
block={props.block as UrlInputBlock}
|
||||||
|
defaultValue={props.block.prefilledValue}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
hasGuestAvatar={props.hasGuestAvatar}
|
hasGuestAvatar={props.hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.PHONE}>
|
<Match when={props.block.type === InputBlockType.PHONE}>
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
block={
|
block={props.block as PhoneNumberInputBlock}
|
||||||
props.block as PhoneNumberInputBlock & { prefilledValue?: string }
|
defaultValue={props.block.prefilledValue}
|
||||||
}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
hasGuestAvatar={props.hasGuestAvatar}
|
hasGuestAvatar={props.hasGuestAvatar}
|
||||||
/>
|
/>
|
||||||
@ -146,7 +149,8 @@ const Input = (props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.RATING}>
|
<Match when={props.block.type === InputBlockType.RATING}>
|
||||||
<RatingForm
|
<RatingForm
|
||||||
block={props.block as RatingInputBlock & { prefilledValue?: string }}
|
block={props.block as RatingInputBlock}
|
||||||
|
defaultValue={props.block.prefilledValue}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
|
@ -38,7 +38,7 @@ export const LiteBadge = (props: Props) => {
|
|||||||
href={'https://www.typebot.io/?utm_source=litebadge'}
|
href={'https://www.typebot.io/?utm_source=litebadge'}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="fixed py-1 px-2 bg-white z-50 rounded shadow-md lite-badge"
|
class="absolute py-1 px-2 bg-white z-50 rounded shadow-md lite-badge text-gray-900"
|
||||||
style={{ bottom: '20px' }}
|
style={{ bottom: '20px' }}
|
||||||
id="lite-badge"
|
id="lite-badge"
|
||||||
>
|
>
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
import { isNotEmpty } from 'utils'
|
||||||
import { DefaultAvatar } from './DefaultAvatar'
|
import { DefaultAvatar } from './DefaultAvatar'
|
||||||
|
|
||||||
export const Avatar = (props: { avatarSrc?: string }) => (
|
export const Avatar = (props: { avatarSrc?: string }) => (
|
||||||
<Show when={props.avatarSrc !== ''}>
|
<Show
|
||||||
<Show when={props.avatarSrc} keyed fallback={() => <DefaultAvatar />}>
|
when={isNotEmpty(props.avatarSrc)}
|
||||||
{(currentAvatarSrc) => (
|
keyed
|
||||||
<figure
|
fallback={() => <DefaultAvatar />}
|
||||||
class={
|
>
|
||||||
'flex justify-center items-center rounded-full text-white relative animate-fade-in ' +
|
<figure
|
||||||
(isMobile() ? 'w-6 h-6 text-sm' : 'w-10 h-10 text-xl')
|
class={
|
||||||
}
|
'flex justify-center items-center rounded-full text-white relative animate-fade-in ' +
|
||||||
>
|
(isMobile() ? 'w-6 h-6 text-sm' : 'w-10 h-10 text-xl')
|
||||||
<img
|
}
|
||||||
src={currentAvatarSrc}
|
>
|
||||||
alt="Bot avatar"
|
<img
|
||||||
class="rounded-full object-cover w-full h-full"
|
src={props.avatarSrc}
|
||||||
/>
|
alt="Bot avatar"
|
||||||
</figure>
|
class="rounded-full object-cover w-full h-full"
|
||||||
)}
|
/>
|
||||||
</Show>
|
</figure>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { isMobile } from '@/utils/isMobileSignal'
|
|
||||||
import { splitProps } from 'solid-js'
|
import { splitProps } from 'solid-js'
|
||||||
import { JSX } from 'solid-js/jsx-runtime'
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
@ -9,13 +8,13 @@ type ShortTextInputProps = {
|
|||||||
|
|
||||||
export const ShortTextInput = (props: ShortTextInputProps) => {
|
export const ShortTextInput = (props: ShortTextInputProps) => {
|
||||||
const [local, others] = splitProps(props, ['ref', 'onInput'])
|
const [local, others] = splitProps(props, ['ref', 'onInput'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
ref={local.ref}
|
ref={props.ref}
|
||||||
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||||
type="text"
|
type="text"
|
||||||
style={{ 'font-size': '16px' }}
|
style={{ 'font-size': '16px' }}
|
||||||
autofocus={!isMobile()}
|
|
||||||
onInput={(e) => local.onInput(e.currentTarget.value)}
|
onInput={(e) => local.onInput(e.currentTarget.value)}
|
||||||
{...others}
|
{...others}
|
||||||
/>
|
/>
|
||||||
|
@ -3,9 +3,6 @@ import type { Component } from 'solid-js'
|
|||||||
|
|
||||||
export const App: Component = () => {
|
export const App: Component = () => {
|
||||||
return (
|
return (
|
||||||
<Bot
|
<Bot typebot="clbm11cku000t3b6o01ug8awh" apiHost="http://localhost:3001" />
|
||||||
typebotId="clbm11cku000t3b6o01ug8awh"
|
|
||||||
apiHost="http://localhost:3001"
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ type Props = {
|
|||||||
export const DateForm = (props: Props) => {
|
export const DateForm = (props: Props) => {
|
||||||
const [inputValues, setInputValues] = createSignal({ from: '', to: '' })
|
const [inputValues, setInputValues] = createSignal({ from: '', to: '' })
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full lg:w-4/6">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<form
|
<form
|
||||||
class={'flex justify-between rounded-lg typebot-input pr-2 items-end'}
|
class={'flex justify-between rounded-lg typebot-input pr-2 items-end'}
|
||||||
|
@ -20,7 +20,6 @@ export const parseReadableDate = ({
|
|||||||
const fromReadable = new Date(
|
const fromReadable = new Date(
|
||||||
hasTime ? from : from.replace(/-/g, '/')
|
hasTime ? from : from.replace(/-/g, '/')
|
||||||
).toLocaleString(currentLocale, formatOptions)
|
).toLocaleString(currentLocale, formatOptions)
|
||||||
console.log(to, to.replace(/-/g, '/'))
|
|
||||||
const toReadable = new Date(
|
const toReadable = new Date(
|
||||||
hasTime ? to : to.replace(/-/g, '/')
|
hasTime ? to : to.replace(/-/g, '/')
|
||||||
).toLocaleString(currentLocale, formatOptions)
|
).toLocaleString(currentLocale, formatOptions)
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { ShortTextInput } from '@/components/inputs'
|
import { ShortTextInput } from '@/components/inputs'
|
||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { EmailInputBlock } from 'models'
|
import { EmailInputBlock } from 'models'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: EmailInputBlock & { prefilledValue?: string }
|
block: EmailInputBlock
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
defaultValue?: string
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmailInput = (props: Props) => {
|
export const EmailInput = (props: Props) => {
|
||||||
const [inputValue, setInputValue] = createSignal(
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
props.block.prefilledValue ?? ''
|
|
||||||
)
|
|
||||||
let inputRef: HTMLInputElement | undefined
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||||
@ -30,6 +29,10 @@ export const EmailInput = (props: Props) => {
|
|||||||
if (e.key === 'Enter') submit()
|
if (e.key === 'Enter') submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { ShortTextInput } from '@/components/inputs'
|
import { ShortTextInput } from '@/components/inputs'
|
||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { NumberInputBlock } from 'models'
|
import { NumberInputBlock } from 'models'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
type NumberInputProps = {
|
type NumberInputProps = {
|
||||||
block: NumberInputBlock & { prefilledValue?: string }
|
block: NumberInputBlock
|
||||||
|
defaultValue?: string
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberInput = (props: NumberInputProps) => {
|
export const NumberInput = (props: NumberInputProps) => {
|
||||||
const [inputValue, setInputValue] = createSignal(
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
props.block.prefilledValue ?? ''
|
|
||||||
)
|
|
||||||
let inputRef: HTMLInputElement | undefined
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||||
@ -30,6 +29,10 @@ export const NumberInput = (props: NumberInputProps) => {
|
|||||||
if (e.key === 'Enter') submit()
|
if (e.key === 'Enter') submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
|
@ -3,21 +3,19 @@ import { SendButton } from '@/components/SendButton'
|
|||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import type { PhoneNumberInputBlock } from 'models'
|
import type { PhoneNumberInputBlock } from 'models'
|
||||||
import { createSignal, For } from 'solid-js'
|
import { createSignal, For, onMount } from 'solid-js'
|
||||||
import { phoneCountries } from 'utils/phoneCountries'
|
import { phoneCountries } from 'utils/phoneCountries'
|
||||||
|
|
||||||
type PhoneInputProps = {
|
type PhoneInputProps = {
|
||||||
block: PhoneNumberInputBlock & { prefilledValue?: string }
|
block: PhoneNumberInputBlock
|
||||||
|
defaultValue?: string
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PhoneInput = (props: PhoneInputProps) => {
|
export const PhoneInput = (props: PhoneInputProps) => {
|
||||||
const [selectedCountryCode, setSelectedCountryCode] = createSignal('INT')
|
const [selectedCountryCode, setSelectedCountryCode] = createSignal('INT')
|
||||||
const [inputValue, setInputValue] = createSignal(
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
props.block.prefilledValue ?? ''
|
|
||||||
)
|
|
||||||
let inputRef: HTMLInputElement | undefined
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string | undefined) => {
|
const handleInput = (inputValue: string | undefined) => {
|
||||||
@ -47,11 +45,13 @@ export const PhoneInput = (props: PhoneInputProps) => {
|
|||||||
setSelectedCountryCode(event.currentTarget.value)
|
setSelectedCountryCode(event.currentTarget.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={'flex items-end justify-between rounded-lg pr-2 typebot-input'}
|
||||||
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
|
||||||
}
|
|
||||||
data-testid="input"
|
data-testid="input"
|
||||||
style={{
|
style={{
|
||||||
'margin-right': props.hasGuestAvatar ? '50px' : '0.5rem',
|
'margin-right': props.hasGuestAvatar ? '50px' : '0.5rem',
|
||||||
|
@ -5,20 +5,21 @@ import { createSignal, For, Match, Switch } from 'solid-js'
|
|||||||
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: RatingInputBlock & { prefilledValue?: string }
|
block: RatingInputBlock
|
||||||
|
defaultValue?: string
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingForm = (props: Props) => {
|
export const RatingForm = (props: Props) => {
|
||||||
const [rating, setRating] = createSignal<number | undefined>(
|
const [rating, setRating] = createSignal<number | undefined>(
|
||||||
// eslint-disable-next-line solid/reactivity
|
props.defaultValue ? Number(props.defaultValue) : undefined
|
||||||
props.block.prefilledValue ? Number(props.block.prefilledValue) : undefined
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSubmit = (e: SubmitEvent) => {
|
const handleSubmit = (e: SubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (isNotDefined(rating)) return
|
const selectedRating = rating()
|
||||||
props.onSubmit({ value: rating.toString() })
|
if (isNotDefined(selectedRating)) return
|
||||||
|
props.onSubmit({ value: selectedRating.toString() })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = (rating: number) => {
|
const handleClick = (rating: number) => {
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { Textarea, ShortTextInput } from '@/components/inputs'
|
import { Textarea, ShortTextInput } from '@/components/inputs'
|
||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { TextInputBlock } from 'models'
|
import { TextInputBlock } from 'models'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: TextInputBlock & { prefilledValue?: string }
|
block: TextInputBlock
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
defaultValue?: string
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInput = (props: Props) => {
|
export const TextInput = (props: Props) => {
|
||||||
const [inputValue, setInputValue] = createSignal(
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
props.block.prefilledValue ?? ''
|
|
||||||
)
|
|
||||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||||
@ -31,6 +30,10 @@ export const TextInput = (props: Props) => {
|
|||||||
if (e.key === 'Enter') submit()
|
if (e.key === 'Enter') submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { ShortTextInput } from '@/components/inputs'
|
import { ShortTextInput } from '@/components/inputs'
|
||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { UrlInputBlock } from 'models'
|
import { UrlInputBlock } from 'models'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: UrlInputBlock & { prefilledValue?: string }
|
block: UrlInputBlock
|
||||||
|
defaultValue?: string
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UrlInput = (props: Props) => {
|
export const UrlInput = (props: Props) => {
|
||||||
const [inputValue, setInputValue] = createSignal(
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
props.block.prefilledValue ?? ''
|
|
||||||
)
|
|
||||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string) => {
|
const handleInput = (inputValue: string) => {
|
||||||
@ -36,6 +35,10 @@ export const UrlInput = (props: Props) => {
|
|||||||
if (e.key === 'Enter') submit()
|
if (e.key === 'Enter') submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
|
@ -20,7 +20,7 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
'onClose',
|
'onClose',
|
||||||
'previewMessage',
|
'previewMessage',
|
||||||
'onPreviewMessageClick',
|
'onPreviewMessageClick',
|
||||||
'button',
|
'theme',
|
||||||
])
|
])
|
||||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||||
// eslint-disable-next-line solid/reactivity
|
// eslint-disable-next-line solid/reactivity
|
||||||
@ -106,13 +106,13 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
<Show when={isPreviewMessageDisplayed()}>
|
<Show when={isPreviewMessageDisplayed()}>
|
||||||
<PreviewMessage
|
<PreviewMessage
|
||||||
{...previewMessage()}
|
{...previewMessage()}
|
||||||
button={bubbleProps.button}
|
previewMessageTheme={bubbleProps.theme?.previewMessage}
|
||||||
onClick={handlePreviewMessageClick}
|
onClick={handlePreviewMessageClick}
|
||||||
onCloseClick={hideMessage}
|
onCloseClick={hideMessage}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<BubbleButton
|
<BubbleButton
|
||||||
{...bubbleProps.button}
|
{...bubbleProps.theme?.button}
|
||||||
toggleBot={toggleBot}
|
toggleBot={toggleBot}
|
||||||
isBotOpened={isBotOpened()}
|
isBotOpened={isBotOpened()}
|
||||||
/>
|
/>
|
||||||
@ -126,7 +126,7 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
||||||
}}
|
}}
|
||||||
class={
|
class={
|
||||||
'absolute bottom-20 sm:right-4 rounded-lg bg-white w-full sm:w-[400px] max-h-[704px] ' +
|
'absolute bottom-20 sm:right-4 rounded-lg w-full sm:w-[400px] max-h-[704px] ' +
|
||||||
(isBotOpened() ? 'opacity-1' : 'opacity-0 pointer-events-none')
|
(isBotOpened() ? 'opacity-1' : 'opacity-0 pointer-events-none')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
import { ButtonParams } from '../types'
|
import { ButtonTheme } from '../types'
|
||||||
|
|
||||||
type Props = ButtonParams & {
|
type Props = ButtonTheme & {
|
||||||
isBotOpened: boolean
|
isBotOpened: boolean
|
||||||
toggleBot: () => void
|
toggleBot: () => void
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { BubbleParams, PreviewMessageParams } from '../types'
|
import { PreviewMessageParams, PreviewMessageTheme } from '../types'
|
||||||
|
|
||||||
export type PreviewMessageProps = Pick<
|
export type PreviewMessageProps = Pick<
|
||||||
PreviewMessageParams,
|
PreviewMessageParams,
|
||||||
'avatarUrl' | 'message' | 'style'
|
'avatarUrl' | 'message'
|
||||||
> &
|
> & {
|
||||||
Pick<BubbleParams, 'button'> & {
|
previewMessageTheme?: PreviewMessageTheme
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onCloseClick: () => void
|
onCloseClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultFontFamily =
|
const defaultFontFamily =
|
||||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
|
||||||
@ -23,9 +23,11 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
class="absolute bottom-20 right-4 w-64 rounded-md duration-200 flex items-center gap-4 shadow-md animate-fade-in cursor-pointer hover:shadow-lg p-4"
|
class="absolute bottom-20 right-4 w-64 rounded-md duration-200 flex items-center gap-4 shadow-md animate-fade-in cursor-pointer hover:shadow-lg p-4"
|
||||||
style={{
|
style={{
|
||||||
'font-family': props.style?.fontFamily ?? defaultFontFamily,
|
'font-family':
|
||||||
'background-color': props.style?.backgroundColor ?? '#F7F8FF',
|
props.previewMessageTheme?.fontFamily ?? defaultFontFamily,
|
||||||
color: props.style?.color ?? '#303235',
|
'background-color':
|
||||||
|
props.previewMessageTheme?.backgroundColor ?? '#F7F8FF',
|
||||||
|
color: props.previewMessageTheme?.color ?? '#303235',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setIsPreviewMessageHovered(true)}
|
onMouseEnter={() => setIsPreviewMessageHovered(true)}
|
||||||
onMouseLeave={() => setIsPreviewMessageHovered(false)}
|
onMouseLeave={() => setIsPreviewMessageHovered(false)}
|
||||||
@ -40,8 +42,9 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
|
|||||||
return props.onCloseClick()
|
return props.onCloseClick()
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
'background-color': props.style?.closeButtonBgColor ?? '#F7F8FF',
|
'background-color':
|
||||||
color: props.style?.closeButtonColor ?? '#303235',
|
props.previewMessageTheme?.closeButtonBgColor ?? '#F7F8FF',
|
||||||
|
color: props.previewMessageTheme?.closeButtonColor ?? '#303235',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
export type BubbleParams = {
|
export type BubbleParams = {
|
||||||
button: ButtonParams
|
theme?: BubbleTheme
|
||||||
previewMessage: PreviewMessageParams
|
previewMessage?: PreviewMessageParams
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonParams = {
|
export type BubbleTheme = {
|
||||||
|
button?: ButtonTheme
|
||||||
|
previewMessage?: PreviewMessageTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ButtonTheme = {
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
icon?: {
|
icon?: {
|
||||||
color?: string
|
color?: string
|
||||||
@ -15,13 +20,12 @@ export type PreviewMessageParams = {
|
|||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
message: string
|
message: string
|
||||||
autoShowDelay?: number
|
autoShowDelay?: number
|
||||||
style?: PreviewMessageStyle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreviewMessageStyle = Partial<{
|
export type PreviewMessageTheme = {
|
||||||
backgroundColor: string
|
backgroundColor?: string
|
||||||
color: string
|
color?: string
|
||||||
fontFamily: string
|
fontFamily?: string
|
||||||
closeButtonBgColor: string
|
closeButtonBgColor?: string
|
||||||
closeButtonColor: string
|
closeButtonColor?: string
|
||||||
}>
|
}
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import styles from '../../../assets/index.css'
|
import styles from '../../../assets/index.css'
|
||||||
import { createSignal, onMount, Show, splitProps, onCleanup } from 'solid-js'
|
import {
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
splitProps,
|
||||||
|
onCleanup,
|
||||||
|
createEffect,
|
||||||
|
} from 'solid-js'
|
||||||
import { Bot, BotProps } from '../../../components/Bot'
|
import { Bot, BotProps } from '../../../components/Bot'
|
||||||
import { CommandData } from '@/features/commands'
|
import { CommandData } from '@/features/commands'
|
||||||
import { isDefined } from 'utils'
|
import { isDefined } from 'utils'
|
||||||
@ -7,6 +14,8 @@ import { PopupParams } from '../types'
|
|||||||
|
|
||||||
export type PopupProps = BotProps &
|
export type PopupProps = BotProps &
|
||||||
PopupParams & {
|
PopupParams & {
|
||||||
|
defaultOpen?: boolean
|
||||||
|
isOpen?: boolean
|
||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
@ -18,7 +27,9 @@ export const Popup = (props: PopupProps) => {
|
|||||||
'onOpen',
|
'onOpen',
|
||||||
'onClose',
|
'onClose',
|
||||||
'autoShowDelay',
|
'autoShowDelay',
|
||||||
'style',
|
'theme',
|
||||||
|
'isOpen',
|
||||||
|
'defaultOpen',
|
||||||
])
|
])
|
||||||
|
|
||||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||||
@ -26,10 +37,14 @@ export const Popup = (props: PopupProps) => {
|
|||||||
botProps.prefilledVariables
|
botProps.prefilledVariables
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
const [isBotOpened, setIsBotOpened] = createSignal(
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
popupProps.isOpen ?? popupProps.defaultOpen ?? false
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('click', processWindowClick)
|
document.addEventListener('pointerdown', processWindowClick)
|
||||||
|
botContainer?.addEventListener('pointerdown', stopPropagation)
|
||||||
window.addEventListener('message', processIncomingEvent)
|
window.addEventListener('message', processIncomingEvent)
|
||||||
const autoShowDelay = popupProps.autoShowDelay
|
const autoShowDelay = popupProps.autoShowDelay
|
||||||
if (isDefined(autoShowDelay)) {
|
if (isDefined(autoShowDelay)) {
|
||||||
@ -39,16 +54,25 @@ export const Popup = (props: PopupProps) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
createEffect(() => {
|
||||||
window.removeEventListener('message', processIncomingEvent)
|
const isOpen = popupProps.isOpen
|
||||||
window.removeEventListener('click', processWindowClick)
|
if (isDefined(isOpen)) setIsBotOpened(isOpen)
|
||||||
})
|
})
|
||||||
|
|
||||||
const processWindowClick = (event: MouseEvent) => {
|
onCleanup(() => {
|
||||||
if (!botContainer || botContainer.contains(event.target as Node)) return
|
document.removeEventListener('pointerdown', processWindowClick)
|
||||||
|
botContainer?.removeEventListener('pointerdown', stopPropagation)
|
||||||
|
window.removeEventListener('message', processIncomingEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
const processWindowClick = () => {
|
||||||
setIsBotOpened(false)
|
setIsBotOpened(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopPropagation = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
const processIncomingEvent = (event: MessageEvent<CommandData>) => {
|
const processIncomingEvent = (event: MessageEvent<CommandData>) => {
|
||||||
const { data } = event
|
const { data } = event
|
||||||
if (!data.isFromTypebot) return
|
if (!data.isFromTypebot) return
|
||||||
@ -63,16 +87,19 @@ export const Popup = (props: PopupProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openBot = () => {
|
const openBot = () => {
|
||||||
setIsBotOpened(true)
|
|
||||||
if (isBotOpened()) popupProps.onOpen?.()
|
if (isBotOpened()) popupProps.onOpen?.()
|
||||||
|
if (isDefined(props.isOpen)) return
|
||||||
|
setIsBotOpened(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeBot = () => {
|
const closeBot = () => {
|
||||||
setIsBotOpened(false)
|
|
||||||
if (isBotOpened()) popupProps.onClose?.()
|
if (isBotOpened()) popupProps.onClose?.()
|
||||||
|
if (isDefined(props.isOpen)) return
|
||||||
|
setIsBotOpened(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleBot = () => {
|
const toggleBot = () => {
|
||||||
|
if (isDefined(props.isOpen)) return
|
||||||
isBotOpened() ? closeBot() : openBot()
|
isBotOpened() ? closeBot() : openBot()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,15 +112,11 @@ export const Popup = (props: PopupProps) => {
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<style>{styles}</style>
|
<style>{styles}</style>
|
||||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity animate-fade-in" />
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity animate-fade-in" />
|
||||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
<div
|
<div
|
||||||
class="relative h-[80vh] transform overflow-hidden rounded-lg text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
|
class="relative h-[80vh] transform overflow-hidden rounded-lg text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
|
||||||
style={{
|
|
||||||
width: popupProps.style?.width ?? '100%',
|
|
||||||
'background-color': popupProps.style?.backgroundColor ?? '#fff',
|
|
||||||
}}
|
|
||||||
ref={botContainer}
|
ref={botContainer}
|
||||||
>
|
>
|
||||||
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
|
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export type PopupParams = {
|
export type PopupParams = {
|
||||||
autoShowDelay?: number
|
autoShowDelay?: number
|
||||||
style?: {
|
theme?: {
|
||||||
width?: string
|
width?: string
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,31 @@
|
|||||||
import { InitialChatReply, SendMessageInput, StartParams } from 'models'
|
import { InitialChatReply } from '@/types'
|
||||||
import { getViewerUrl, sendRequest } from 'utils'
|
import { SendMessageInput, StartParams } from 'models'
|
||||||
|
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
|
||||||
|
|
||||||
export async function getInitialChatReplyQuery({
|
export async function getInitialChatReplyQuery({
|
||||||
typebotId,
|
typebot,
|
||||||
isPreview,
|
isPreview,
|
||||||
apiHost,
|
apiHost,
|
||||||
prefilledVariables,
|
prefilledVariables,
|
||||||
|
startGroupId,
|
||||||
|
resultId,
|
||||||
}: StartParams & {
|
}: StartParams & {
|
||||||
apiHost?: string
|
apiHost?: string
|
||||||
}) {
|
}) {
|
||||||
if (!typebotId)
|
if (!typebot)
|
||||||
throw new Error('Typebot ID is required to get initial messages')
|
throw new Error('Typebot ID is required to get initial messages')
|
||||||
|
|
||||||
const response = await sendRequest<InitialChatReply>({
|
return sendRequest<InitialChatReply>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
|
url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
|
||||||
body: {
|
body: {
|
||||||
startParams: {
|
startParams: {
|
||||||
isPreview,
|
isPreview,
|
||||||
typebotId,
|
typebot,
|
||||||
prefilledVariables,
|
prefilledVariables,
|
||||||
|
startGroupId,
|
||||||
|
resultId,
|
||||||
},
|
},
|
||||||
} satisfies SendMessageInput,
|
} satisfies SendMessageInput,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChatReply, SendMessageInput } from 'models'
|
import { ChatReply, SendMessageInput } from 'models'
|
||||||
import { getViewerUrl, sendRequest } from 'utils'
|
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
|
||||||
|
|
||||||
export async function sendMessageQuery({
|
export async function sendMessageQuery({
|
||||||
apiHost,
|
apiHost,
|
||||||
@ -7,7 +7,7 @@ export async function sendMessageQuery({
|
|||||||
}: SendMessageInput & { apiHost?: string }) {
|
}: SendMessageInput & { apiHost?: string }) {
|
||||||
const response = await sendRequest<ChatReply>({
|
const response = await sendRequest<ChatReply>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
|
url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ChatReply } from 'models'
|
||||||
|
|
||||||
export type InputSubmitContent = {
|
export type InputSubmitContent = {
|
||||||
label?: string
|
label?: string
|
||||||
value: string
|
value: string
|
||||||
@ -5,7 +7,12 @@ export type InputSubmitContent = {
|
|||||||
|
|
||||||
export type BotContext = {
|
export type BotContext = {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
resultId: string
|
resultId?: string
|
||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
apiHost?: string
|
apiHost?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InitialChatReply = ChatReply & {
|
||||||
|
typebot: NonNullable<ChatReply['typebot']>
|
||||||
|
sessionId: NonNullable<ChatReply['sessionId']>
|
||||||
|
}
|
||||||
|
17
packages/js/src/utils/sessionStorage.ts
Normal file
17
packages/js/src/utils/sessionStorage.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const sessionStorageKey = 'resultId'
|
||||||
|
|
||||||
|
export const getExistingResultIdFromSession = () => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(sessionStorageKey) ?? undefined
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setResultInSession = (resultId: string) => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.setItem(sessionStorageKey, resultId)
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
144
packages/js/src/utils/setCssVariablesValue.ts
Normal file
144
packages/js/src/utils/setCssVariablesValue.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
Background,
|
||||||
|
BackgroundType,
|
||||||
|
ChatTheme,
|
||||||
|
ContainerColors,
|
||||||
|
GeneralTheme,
|
||||||
|
InputColors,
|
||||||
|
Theme,
|
||||||
|
} from 'models'
|
||||||
|
|
||||||
|
const cssVariableNames = {
|
||||||
|
general: {
|
||||||
|
bgImage: '--typebot-container-bg-image',
|
||||||
|
bgColor: '--typebot-container-bg-color',
|
||||||
|
fontFamily: '--typebot-container-font-family',
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
hostBubbles: {
|
||||||
|
bgColor: '--typebot-host-bubble-bg-color',
|
||||||
|
color: '--typebot-host-bubble-color',
|
||||||
|
},
|
||||||
|
guestBubbles: {
|
||||||
|
bgColor: '--typebot-guest-bubble-bg-color',
|
||||||
|
color: '--typebot-guest-bubble-color',
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
bgColor: '--typebot-input-bg-color',
|
||||||
|
color: '--typebot-input-color',
|
||||||
|
placeholderColor: '--typebot-input-placeholder-color',
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
bgColor: '--typebot-button-bg-color',
|
||||||
|
color: '--typebot-button-color',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setCssVariablesValue = (
|
||||||
|
theme: Theme | undefined,
|
||||||
|
container: HTMLDivElement
|
||||||
|
) => {
|
||||||
|
if (!theme) return
|
||||||
|
const documentStyle = container?.style
|
||||||
|
if (!documentStyle) return
|
||||||
|
if (theme.general) setGeneralTheme(theme.general, documentStyle)
|
||||||
|
if (theme.chat) setChatTheme(theme.chat, documentStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setGeneralTheme = (
|
||||||
|
generalTheme: GeneralTheme,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
const { background, font } = generalTheme
|
||||||
|
if (background) setTypebotBackground(background, documentStyle)
|
||||||
|
if (font) documentStyle.setProperty(cssVariableNames.general.fontFamily, font)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setChatTheme = (
|
||||||
|
chatTheme: ChatTheme,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
const { hostBubbles, guestBubbles, buttons, inputs } = chatTheme
|
||||||
|
if (hostBubbles) setHostBubbles(hostBubbles, documentStyle)
|
||||||
|
if (guestBubbles) setGuestBubbles(guestBubbles, documentStyle)
|
||||||
|
if (buttons) setButtons(buttons, documentStyle)
|
||||||
|
if (inputs) setInputs(inputs, documentStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setHostBubbles = (
|
||||||
|
hostBubbles: ContainerColors,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
if (hostBubbles.backgroundColor)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.hostBubbles.bgColor,
|
||||||
|
hostBubbles.backgroundColor
|
||||||
|
)
|
||||||
|
if (hostBubbles.color)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.hostBubbles.color,
|
||||||
|
hostBubbles.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setGuestBubbles = (
|
||||||
|
guestBubbles: ContainerColors,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
if (guestBubbles.backgroundColor)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.guestBubbles.bgColor,
|
||||||
|
guestBubbles.backgroundColor
|
||||||
|
)
|
||||||
|
if (guestBubbles.color)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.guestBubbles.color,
|
||||||
|
guestBubbles.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setButtons = (
|
||||||
|
buttons: ContainerColors,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
if (buttons.backgroundColor)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.buttons.bgColor,
|
||||||
|
buttons.backgroundColor
|
||||||
|
)
|
||||||
|
if (buttons.color)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.buttons.color,
|
||||||
|
buttons.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => {
|
||||||
|
if (inputs.backgroundColor)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.inputs.bgColor,
|
||||||
|
inputs.backgroundColor
|
||||||
|
)
|
||||||
|
if (inputs.color)
|
||||||
|
documentStyle.setProperty(cssVariableNames.chat.inputs.color, inputs.color)
|
||||||
|
if (inputs.placeholderColor)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.inputs.placeholderColor,
|
||||||
|
inputs.placeholderColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTypebotBackground = (
|
||||||
|
background: Background,
|
||||||
|
documentStyle: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
documentStyle.setProperty(
|
||||||
|
background?.type === BackgroundType.IMAGE
|
||||||
|
? cssVariableNames.general.bgImage
|
||||||
|
: cssVariableNames.general.bgColor,
|
||||||
|
background.type === BackgroundType.NONE
|
||||||
|
? 'transparent'
|
||||||
|
: background.content ?? '#ffffff'
|
||||||
|
)
|
||||||
|
}
|
@ -24,8 +24,14 @@ const typebotInSessionStateSchema = publicTypebotSchema.pick({
|
|||||||
variables: true,
|
variables: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const dynamicThemeSchema = z.object({
|
||||||
|
hostAvatarUrl: z.string().optional(),
|
||||||
|
guestAvatarUrl: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const sessionStateSchema = z.object({
|
export const sessionStateSchema = z.object({
|
||||||
typebot: typebotInSessionStateSchema,
|
typebot: typebotInSessionStateSchema,
|
||||||
|
dynamicTheme: dynamicThemeSchema.optional(),
|
||||||
linkedTypebots: z.object({
|
linkedTypebots: z.object({
|
||||||
typebots: z.array(typebotInSessionStateSchema),
|
typebots: z.array(typebotInSessionStateSchema),
|
||||||
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
|
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
|
||||||
@ -95,11 +101,21 @@ const codeToExecuteSchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const startTypebotSchema = typebotSchema.pick({
|
||||||
|
id: true,
|
||||||
|
groups: true,
|
||||||
|
edges: true,
|
||||||
|
variables: true,
|
||||||
|
settings: true,
|
||||||
|
theme: true,
|
||||||
|
})
|
||||||
|
|
||||||
const startParamsSchema = z.object({
|
const startParamsSchema = z.object({
|
||||||
typebotId: z.string({
|
typebot: startTypebotSchema
|
||||||
description:
|
.or(z.string())
|
||||||
'[How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)',
|
.describe(
|
||||||
}),
|
'Either a Typebot ID or a Typebot object. If you provide a Typebot object, it will be executed in preview mode. ([How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)).'
|
||||||
|
),
|
||||||
isPreview: z
|
isPreview: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
@ -110,7 +126,16 @@ const startParamsSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Provide it if you'd like to overwrite an existing result."),
|
.describe("Provide it if you'd like to overwrite an existing result."),
|
||||||
prefilledVariables: z.record(z.unknown()).optional(),
|
startGroupId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Start chat from a specific group.'),
|
||||||
|
prefilledVariables: z
|
||||||
|
.record(z.unknown())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)'
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sendMessageInputSchema = z.object({
|
export const sendMessageInputSchema = z.object({
|
||||||
@ -158,25 +183,20 @@ export const chatReplySchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
sessionId: z.string().optional(),
|
sessionId: z.string().optional(),
|
||||||
typebot: typebotSchema.pick({ theme: true, settings: true }).optional(),
|
typebot: typebotSchema
|
||||||
|
.pick({ id: true, theme: true, settings: true })
|
||||||
|
.optional(),
|
||||||
resultId: z.string().optional(),
|
resultId: z.string().optional(),
|
||||||
|
dynamicTheme: dynamicThemeSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const initialChatReplySchema = z
|
|
||||||
.object({
|
|
||||||
sessionId: z.string(),
|
|
||||||
resultId: z.string(),
|
|
||||||
typebot: typebotSchema.pick({ theme: true, settings: true }),
|
|
||||||
})
|
|
||||||
.and(chatReplySchema)
|
|
||||||
|
|
||||||
export type ChatSession = z.infer<typeof chatSessionSchema>
|
export type ChatSession = z.infer<typeof chatSessionSchema>
|
||||||
export type SessionState = z.infer<typeof sessionStateSchema>
|
export type SessionState = z.infer<typeof sessionStateSchema>
|
||||||
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
|
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
|
||||||
export type ChatReply = z.infer<typeof chatReplySchema>
|
export type ChatReply = z.infer<typeof chatReplySchema>
|
||||||
export type InitialChatReply = z.infer<typeof initialChatReplySchema>
|
|
||||||
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>
|
||||||
export type CodeToExecute = z.infer<typeof codeToExecuteSchema>
|
export type CodeToExecute = z.infer<typeof codeToExecuteSchema>
|
||||||
export type StartParams = z.infer<typeof startParamsSchema>
|
export type StartParams = z.infer<typeof startParamsSchema>
|
||||||
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema>
|
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema>
|
||||||
|
export type StartTypebot = z.infer<typeof startTypebotSchema>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "React library to display typebots on your website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import type { BubbleProps } from '@typebot.io/js'
|
import type { BubbleProps } from '@typebot.io/js'
|
||||||
|
import { defaultBubbleProps } from './constants'
|
||||||
|
|
||||||
|
type Props = BubbleProps & { style?: React.CSSProperties; className?: string }
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
@ -7,18 +10,54 @@ declare global {
|
|||||||
'typebot-bubble': React.DetailedHTMLProps<
|
'typebot-bubble': React.DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLElement>,
|
React.HTMLAttributes<HTMLElement>,
|
||||||
HTMLElement
|
HTMLElement
|
||||||
>
|
> & { class?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Bubble = (props: BubbleProps) => {
|
export const Bubble = ({ style, className, ...props }: Props) => {
|
||||||
|
const ref = useRef<(HTMLDivElement & Props) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { registerBubbleComponent } = await import('@typebot.io/js')
|
const { registerBubbleComponent } = await import('@typebot.io/js')
|
||||||
registerBubbleComponent(props)
|
registerBubbleComponent(defaultBubbleProps)
|
||||||
})()
|
})()
|
||||||
}, [props])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <typebot-bubble />
|
useEffect(() => {
|
||||||
|
if (!ref.current) return
|
||||||
|
ref.current.typebot = props.typebot
|
||||||
|
ref.current.prefilledVariables = props.prefilledVariables
|
||||||
|
ref.current.onClose = props.onClose
|
||||||
|
ref.current.onOpen = props.onOpen
|
||||||
|
ref.current.onNewInputBlock = props.onNewInputBlock
|
||||||
|
ref.current.onAnswer = props.onAnswer
|
||||||
|
ref.current.onPreviewMessageClick = props.onPreviewMessageClick
|
||||||
|
ref.current.onEnd = props.onEnd
|
||||||
|
ref.current.onInit = props.onInit
|
||||||
|
}, [
|
||||||
|
props.onAnswer,
|
||||||
|
props.onClose,
|
||||||
|
props.onNewInputBlock,
|
||||||
|
props.onOpen,
|
||||||
|
props.onPreviewMessageClick,
|
||||||
|
props.prefilledVariables,
|
||||||
|
props.typebot,
|
||||||
|
props.onEnd,
|
||||||
|
props.onInit,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<typebot-bubble
|
||||||
|
ref={ref}
|
||||||
|
api-host={props.apiHost}
|
||||||
|
start-group-id={props.startGroupId}
|
||||||
|
result-id={props.resultId}
|
||||||
|
is-preview={props.isPreview}
|
||||||
|
class={className}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import type { PopupProps } from '@typebot.io/js'
|
import type { PopupProps } from '@typebot.io/js'
|
||||||
|
import { defaultPopupProps } from './constants'
|
||||||
|
|
||||||
|
type Props = PopupProps & { style?: React.CSSProperties; className?: string }
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
@ -7,18 +10,54 @@ declare global {
|
|||||||
'typebot-popup': React.DetailedHTMLProps<
|
'typebot-popup': React.DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLElement>,
|
React.HTMLAttributes<HTMLElement>,
|
||||||
HTMLElement
|
HTMLElement
|
||||||
>
|
> & { class?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = ({ style, className, ...props }: Props) => {
|
||||||
|
const ref = useRef<(HTMLDivElement & Props) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { registerPopupComponent } = await import('@typebot.io/js')
|
const { registerPopupComponent } = await import('@typebot.io/js')
|
||||||
registerPopupComponent(props)
|
registerPopupComponent(defaultPopupProps)
|
||||||
})()
|
})()
|
||||||
}, [props])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <typebot-popup />
|
useEffect(() => {
|
||||||
|
if (!ref.current) return
|
||||||
|
ref.current.typebot = props.typebot
|
||||||
|
ref.current.prefilledVariables = props.prefilledVariables
|
||||||
|
ref.current.onClose = props.onClose
|
||||||
|
ref.current.onOpen = props.onOpen
|
||||||
|
ref.current.onNewInputBlock = props.onNewInputBlock
|
||||||
|
ref.current.onAnswer = props.onAnswer
|
||||||
|
ref.current.onEnd = props.onEnd
|
||||||
|
ref.current.onInit = props.onInit
|
||||||
|
}, [
|
||||||
|
props.onAnswer,
|
||||||
|
props.onClose,
|
||||||
|
props.onEnd,
|
||||||
|
props.onNewInputBlock,
|
||||||
|
props.onOpen,
|
||||||
|
props.onInit,
|
||||||
|
props.prefilledVariables,
|
||||||
|
props.typebot,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<typebot-popup
|
||||||
|
ref={ref}
|
||||||
|
api-host={props.apiHost}
|
||||||
|
start-group-id={props.startGroupId}
|
||||||
|
result-id={props.resultId}
|
||||||
|
is-preview={props.isPreview}
|
||||||
|
is-open={props.isOpen}
|
||||||
|
default-open={props.defaultOpen}
|
||||||
|
class={className}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import type { BotProps } from '@typebot.io/js'
|
import type { BotProps } from '@typebot.io/js'
|
||||||
|
import { defaultBotProps } from './constants'
|
||||||
|
|
||||||
type Props = BotProps
|
type Props = BotProps & { style?: React.CSSProperties; className?: string }
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
@ -9,19 +10,48 @@ declare global {
|
|||||||
'typebot-standard': React.DetailedHTMLProps<
|
'typebot-standard': React.DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLElement>,
|
React.HTMLAttributes<HTMLElement>,
|
||||||
HTMLElement
|
HTMLElement
|
||||||
>
|
> & { class?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Standard = (props: Props) => {
|
export const Standard = ({ style, className, ...props }: Props) => {
|
||||||
|
const ref = useRef<(HTMLDivElement & Props) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { registerStandardComponent } = await import('@typebot.io/js')
|
const { registerStandardComponent } = await import('@typebot.io/js')
|
||||||
registerStandardComponent(props)
|
registerStandardComponent(defaultBotProps)
|
||||||
})()
|
})()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <typebot-standard />
|
useEffect(() => {
|
||||||
|
if (!ref.current) return
|
||||||
|
ref.current.typebot = props.typebot
|
||||||
|
ref.current.prefilledVariables = props.prefilledVariables
|
||||||
|
ref.current.onNewInputBlock = props.onNewInputBlock
|
||||||
|
ref.current.onAnswer = props.onAnswer
|
||||||
|
ref.current.onEnd = props.onEnd
|
||||||
|
ref.current.onInit = props.onInit
|
||||||
|
}, [
|
||||||
|
props.onAnswer,
|
||||||
|
props.onNewInputBlock,
|
||||||
|
props.prefilledVariables,
|
||||||
|
props.typebot,
|
||||||
|
props.onEnd,
|
||||||
|
props.onInit,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<typebot-standard
|
||||||
|
ref={ref}
|
||||||
|
api-host={props.apiHost}
|
||||||
|
start-group-id={props.startGroupId}
|
||||||
|
style={style}
|
||||||
|
class={className}
|
||||||
|
result-id={props.resultId}
|
||||||
|
is-preview={props.isPreview}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
32
packages/react/src/constants.ts
Normal file
32
packages/react/src/constants.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { BotProps, PopupProps, BubbleProps } from '@typebot.io/js'
|
||||||
|
|
||||||
|
export const defaultBotProps: BotProps = {
|
||||||
|
typebot: '',
|
||||||
|
onNewInputBlock: undefined,
|
||||||
|
onAnswer: undefined,
|
||||||
|
onEnd: undefined,
|
||||||
|
onInit: undefined,
|
||||||
|
isPreview: undefined,
|
||||||
|
startGroupId: undefined,
|
||||||
|
prefilledVariables: undefined,
|
||||||
|
apiHost: undefined,
|
||||||
|
resultId: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultPopupProps: PopupProps = {
|
||||||
|
...defaultBotProps,
|
||||||
|
onClose: undefined,
|
||||||
|
onOpen: undefined,
|
||||||
|
theme: undefined,
|
||||||
|
autoShowDelay: undefined,
|
||||||
|
isOpen: undefined,
|
||||||
|
defaultOpen: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultBubbleProps: BubbleProps = {
|
||||||
|
...defaultBotProps,
|
||||||
|
onClose: undefined,
|
||||||
|
onOpen: undefined,
|
||||||
|
theme: undefined,
|
||||||
|
previewMessage: undefined,
|
||||||
|
}
|
@ -5,6 +5,6 @@ import { Popup } from './Popup'
|
|||||||
|
|
||||||
export { Standard, Bubble, Popup }
|
export { Standard, Bubble, Popup }
|
||||||
|
|
||||||
export default { Standard, Bubble, Popup }
|
// export default { Standard, Bubble, Popup }
|
||||||
|
|
||||||
export * from '@typebot.io/js/src/features/commands'
|
export * from '@typebot.io/js/src/features/commands'
|
||||||
|
@ -32,7 +32,7 @@ export const Default = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Bubble
|
<Bubble
|
||||||
typebotId="ladleTypebot"
|
typebot="ladleTypebot"
|
||||||
apiHost="http://localhost:3001"
|
apiHost="http://localhost:3001"
|
||||||
prefilledVariables={{
|
prefilledVariables={{
|
||||||
Name: 'John',
|
Name: 'John',
|
||||||
@ -42,12 +42,15 @@ export const Default = () => {
|
|||||||
message: 'Hello, I am a preview message',
|
message: 'Hello, I am a preview message',
|
||||||
autoShowDelay: 3000,
|
autoShowDelay: 3000,
|
||||||
}}
|
}}
|
||||||
button={{
|
theme={{
|
||||||
backgroundColor: '#FF7537',
|
button: {
|
||||||
icon: {
|
backgroundColor: '#FF7537',
|
||||||
color: 'white',
|
icon: {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
isPreview
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -7,9 +7,10 @@ export const Default = () => {
|
|||||||
<button onClick={open}>Open modal</button>
|
<button onClick={open}>Open modal</button>
|
||||||
<button onClick={toggle}>Toggle modal</button>
|
<button onClick={toggle}>Toggle modal</button>
|
||||||
<Popup
|
<Popup
|
||||||
typebotId="ladleTypebot"
|
typebot="clctayswj000l3b6y2vkh8kwg"
|
||||||
apiHost="http://localhost:3001"
|
apiHost="http://localhost:3001"
|
||||||
autoShowDelay={3000}
|
autoShowDelay={3000}
|
||||||
|
isPreview
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,11 @@ import { Standard } from '..'
|
|||||||
export const Default = () => {
|
export const Default = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '500px' }}>
|
<div style={{ height: '500px' }}>
|
||||||
<Standard typebotId="ladleTypebot" apiHost="http://localhost:3001" />
|
<Standard
|
||||||
|
typebot="ladleTypebot"
|
||||||
|
apiHost="http://localhost:3001"
|
||||||
|
isPreview
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user