diff --git a/packages/js/src/features/popup/types.ts b/packages/js/src/features/popup/types.ts
index d73c74bbe..17971956d 100644
--- a/packages/js/src/features/popup/types.ts
+++ b/packages/js/src/features/popup/types.ts
@@ -1,6 +1,6 @@
export type PopupParams = {
autoShowDelay?: number
- style?: {
+ theme?: {
width?: string
backgroundColor?: string
}
diff --git a/packages/js/src/queries/getInitialChatReplyQuery.ts b/packages/js/src/queries/getInitialChatReplyQuery.ts
index 6f5d79ea9..b624fcb09 100644
--- a/packages/js/src/queries/getInitialChatReplyQuery.ts
+++ b/packages/js/src/queries/getInitialChatReplyQuery.ts
@@ -1,28 +1,31 @@
-import { InitialChatReply, SendMessageInput, StartParams } from 'models'
-import { getViewerUrl, sendRequest } from 'utils'
+import { InitialChatReply } from '@/types'
+import { SendMessageInput, StartParams } from 'models'
+import { getViewerUrl, isEmpty, sendRequest } from 'utils'
export async function getInitialChatReplyQuery({
- typebotId,
+ typebot,
isPreview,
apiHost,
prefilledVariables,
+ startGroupId,
+ resultId,
}: StartParams & {
apiHost?: string
}) {
- if (!typebotId)
+ if (!typebot)
throw new Error('Typebot ID is required to get initial messages')
- const response = await sendRequest({
+ return sendRequest({
method: 'POST',
- url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
+ url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
body: {
startParams: {
isPreview,
- typebotId,
+ typebot,
prefilledVariables,
+ startGroupId,
+ resultId,
},
} satisfies SendMessageInput,
})
-
- return response.data
}
diff --git a/packages/js/src/queries/sendMessageQuery.ts b/packages/js/src/queries/sendMessageQuery.ts
index 1061c93be..98dc0e098 100644
--- a/packages/js/src/queries/sendMessageQuery.ts
+++ b/packages/js/src/queries/sendMessageQuery.ts
@@ -1,5 +1,5 @@
import { ChatReply, SendMessageInput } from 'models'
-import { getViewerUrl, sendRequest } from 'utils'
+import { getViewerUrl, isEmpty, sendRequest } from 'utils'
export async function sendMessageQuery({
apiHost,
@@ -7,7 +7,7 @@ export async function sendMessageQuery({
}: SendMessageInput & { apiHost?: string }) {
const response = await sendRequest({
method: 'POST',
- url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
+ url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
body,
})
diff --git a/packages/js/src/types.ts b/packages/js/src/types.ts
index 516ff2f1e..a5ee3f815 100644
--- a/packages/js/src/types.ts
+++ b/packages/js/src/types.ts
@@ -1,3 +1,5 @@
+import { ChatReply } from 'models'
+
export type InputSubmitContent = {
label?: string
value: string
@@ -5,7 +7,12 @@ export type InputSubmitContent = {
export type BotContext = {
typebotId: string
- resultId: string
+ resultId?: string
isPreview: boolean
apiHost?: string
}
+
+export type InitialChatReply = ChatReply & {
+ typebot: NonNullable
+ sessionId: NonNullable
+}
diff --git a/packages/js/src/utils/sessionStorage.ts b/packages/js/src/utils/sessionStorage.ts
new file mode 100644
index 000000000..063e8c021
--- /dev/null
+++ b/packages/js/src/utils/sessionStorage.ts
@@ -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 */
+ }
+}
diff --git a/packages/js/src/utils/setCssVariablesValue.ts b/packages/js/src/utils/setCssVariablesValue.ts
new file mode 100644
index 000000000..24c119898
--- /dev/null
+++ b/packages/js/src/utils/setCssVariablesValue.ts
@@ -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'
+ )
+}
diff --git a/packages/models/src/features/chat.ts b/packages/models/src/features/chat.ts
index f43282a0a..bb8a63648 100644
--- a/packages/models/src/features/chat.ts
+++ b/packages/models/src/features/chat.ts
@@ -24,8 +24,14 @@ const typebotInSessionStateSchema = publicTypebotSchema.pick({
variables: true,
})
+const dynamicThemeSchema = z.object({
+ hostAvatarUrl: z.string().optional(),
+ guestAvatarUrl: z.string().optional(),
+})
+
export const sessionStateSchema = z.object({
typebot: typebotInSessionStateSchema,
+ dynamicTheme: dynamicThemeSchema.optional(),
linkedTypebots: z.object({
typebots: z.array(typebotInSessionStateSchema),
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
@@ -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({
- typebotId: z.string({
- description:
- '[How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)',
- }),
+ typebot: startTypebotSchema
+ .or(z.string())
+ .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
.boolean()
.optional()
@@ -110,7 +126,16 @@ const startParamsSchema = z.object({
.string()
.optional()
.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({
@@ -158,25 +183,20 @@ export const chatReplySchema = z.object({
})
.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(),
+ 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
export type SessionState = z.infer
export type TypebotInSession = z.infer
export type ChatReply = z.infer
-export type InitialChatReply = z.infer
export type ChatMessage = z.infer
export type SendMessageInput = z.infer
export type CodeToExecute = z.infer
export type StartParams = z.infer
export type RuntimeOptions = z.infer
+export type StartTypebot = z.infer
diff --git a/packages/react/package.json b/packages/react/package.json
index 74305c252..32b8b8d5e 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,7 +1,7 @@
{
"name": "@typebot.io/react",
- "version": "1.0.0",
- "description": "",
+ "version": "0.0.1",
+ "description": "React library to display typebots on your website",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
diff --git a/packages/react/src/Bubble.tsx b/packages/react/src/Bubble.tsx
index 104781f71..27c956d48 100644
--- a/packages/react/src/Bubble.tsx
+++ b/packages/react/src/Bubble.tsx
@@ -1,5 +1,8 @@
-import { useEffect } from 'react'
+import { useEffect, useRef } from 'react'
import type { BubbleProps } from '@typebot.io/js'
+import { defaultBubbleProps } from './constants'
+
+type Props = BubbleProps & { style?: React.CSSProperties; className?: string }
declare global {
namespace JSX {
@@ -7,18 +10,54 @@ declare global {
'typebot-bubble': React.DetailedHTMLProps<
React.HTMLAttributes,
HTMLElement
- >
+ > & { class?: string }
}
}
}
-export const Bubble = (props: BubbleProps) => {
+export const Bubble = ({ style, className, ...props }: Props) => {
+ const ref = useRef<(HTMLDivElement & Props) | null>(null)
+
useEffect(() => {
;(async () => {
const { registerBubbleComponent } = await import('@typebot.io/js')
- registerBubbleComponent(props)
+ registerBubbleComponent(defaultBubbleProps)
})()
- }, [props])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
- return
+ 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 (
+
+ )
}
diff --git a/packages/react/src/Popup.tsx b/packages/react/src/Popup.tsx
index 94bc8770e..da10aed2e 100644
--- a/packages/react/src/Popup.tsx
+++ b/packages/react/src/Popup.tsx
@@ -1,5 +1,8 @@
-import { useEffect } from 'react'
+import { useEffect, useRef } from 'react'
import type { PopupProps } from '@typebot.io/js'
+import { defaultPopupProps } from './constants'
+
+type Props = PopupProps & { style?: React.CSSProperties; className?: string }
declare global {
namespace JSX {
@@ -7,18 +10,54 @@ declare global {
'typebot-popup': React.DetailedHTMLProps<
React.HTMLAttributes,
HTMLElement
- >
+ > & { class?: string }
}
}
}
-export const Popup = (props: PopupProps) => {
+export const Popup = ({ style, className, ...props }: Props) => {
+ const ref = useRef<(HTMLDivElement & Props) | null>(null)
+
useEffect(() => {
;(async () => {
const { registerPopupComponent } = await import('@typebot.io/js')
- registerPopupComponent(props)
+ registerPopupComponent(defaultPopupProps)
})()
- }, [props])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
- return
+ 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 (
+
+ )
}
diff --git a/packages/react/src/Standard.tsx b/packages/react/src/Standard.tsx
index 6a67236b6..4f09e7000 100644
--- a/packages/react/src/Standard.tsx
+++ b/packages/react/src/Standard.tsx
@@ -1,7 +1,8 @@
-import { useEffect } from 'react'
+import { useEffect, useRef } from 'react'
import type { BotProps } from '@typebot.io/js'
+import { defaultBotProps } from './constants'
-type Props = BotProps
+type Props = BotProps & { style?: React.CSSProperties; className?: string }
declare global {
namespace JSX {
@@ -9,19 +10,48 @@ declare global {
'typebot-standard': React.DetailedHTMLProps<
React.HTMLAttributes,
HTMLElement
- >
+ > & { class?: string }
}
}
}
-export const Standard = (props: Props) => {
+export const Standard = ({ style, className, ...props }: Props) => {
+ const ref = useRef<(HTMLDivElement & Props) | null>(null)
+
useEffect(() => {
;(async () => {
const { registerStandardComponent } = await import('@typebot.io/js')
- registerStandardComponent(props)
+ registerStandardComponent(defaultBotProps)
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
- return
+ 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 (
+
+ )
}
diff --git a/packages/react/src/constants.ts b/packages/react/src/constants.ts
new file mode 100644
index 000000000..92715a4dd
--- /dev/null
+++ b/packages/react/src/constants.ts
@@ -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,
+}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 5a2f06850..4dede090d 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -5,6 +5,6 @@ import { Popup } from './Popup'
export { Standard, Bubble, Popup }
-export default { Standard, Bubble, Popup }
+// export default { Standard, Bubble, Popup }
export * from '@typebot.io/js/src/features/commands'
diff --git a/packages/react/src/stories/bubble.stories.tsx b/packages/react/src/stories/bubble.stories.tsx
index 1dd790b61..8217240b4 100644
--- a/packages/react/src/stories/bubble.stories.tsx
+++ b/packages/react/src/stories/bubble.stories.tsx
@@ -32,7 +32,7 @@ export const Default = () => {
{
message: 'Hello, I am a preview message',
autoShowDelay: 3000,
}}
- button={{
- backgroundColor: '#FF7537',
- icon: {
- color: 'white',
+ theme={{
+ button: {
+ backgroundColor: '#FF7537',
+ icon: {
+ color: 'white',
+ },
},
}}
+ isPreview
/>
)
diff --git a/packages/react/src/stories/popup.stories.tsx b/packages/react/src/stories/popup.stories.tsx
index 91d58e6c1..35f21c329 100644
--- a/packages/react/src/stories/popup.stories.tsx
+++ b/packages/react/src/stories/popup.stories.tsx
@@ -7,9 +7,10 @@ export const Default = () => {
>
)
diff --git a/packages/react/src/stories/standard.stories.tsx b/packages/react/src/stories/standard.stories.tsx
index 2b9671409..52aaf8d20 100644
--- a/packages/react/src/stories/standard.stories.tsx
+++ b/packages/react/src/stories/standard.stories.tsx
@@ -3,7 +3,11 @@ import { Standard } from '..'
export const Default = () => {
return (