2
0

(engine) Improve engine overall robustness

This commit is contained in:
Baptiste Arnaud
2023-01-25 11:27:47 +01:00
parent ff62b922a0
commit 30baa611e5
210 changed files with 1820 additions and 1919 deletions

View File

@ -1,7 +1,7 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { DateInputOptions } from 'models'
import React, { useState } from 'react'
import { useState } from 'react'
import { parseReadableDate } from '../utils/parseReadableDate'
type DateInputProps = {

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/demo/index.tsx" type="module"></script>
</body>
</html>

View File

@ -2,20 +2,18 @@
"name": "@typebot.io/js",
"version": "0.0.1",
"description": "Javascript library to display typebots on your website",
"main": "dist/index.mjs",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"start:demo": "vite",
"dev:demo": "vite",
"dev": "rollup --watch --config rollup.config.mjs",
"build": "rollup --config rollup.config.mjs",
"dev": "rollup --watch --config rollup.config.js",
"build": "rollup --config rollup.config.js && rm -rf dist/dts",
"lint": "eslint --fix \"src/**/*.ts*\""
},
"license": "MIT",
"dependencies": {
"@stripe/stripe-js": "1.46.0",
"models": "workspace:*",
"phone": "3.1.32",
"solid-element": "1.6.3",
"solid-js": "1.6.9",
"utils": "workspace:*"
@ -23,9 +21,8 @@
"devDependencies": {
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-node-resolve": "15.0.1",
"@rollup/plugin-replace": "5.0.2",
"@rollup/plugin-terser": "^0.3.0",
"@rollup/plugin-typescript": "11.0.0",
"@types/react": "18.0.27",
"autoprefixer": "10.4.13",
"babel-preset-solid": "1.6.9",
"eslint": "8.32.0",
@ -37,13 +34,9 @@
"rollup-plugin-babel": "4.4.0",
"rollup-plugin-dts": "5.1.1",
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript-paths": "1.4.0",
"rollup-plugin-typescript-paths": "^1.4.0",
"tailwindcss": "3.2.4",
"tsconfig": "workspace:*",
"tsup": "6.5.0",
"typescript": "4.9.4",
"vite": "4.0.4",
"vite-plugin-solid": "2.5.0"
"typescript": "4.9.4"
}
}

View File

@ -1,28 +1,21 @@
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import { terser } from 'rollup-plugin-terser'
import terser from '@rollup/plugin-terser'
import { babel } from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss'
import autoprefixer from 'autoprefixer'
import tailwindcss from 'tailwindcss'
import { typescriptPaths } from 'rollup-plugin-typescript-paths'
import dts from 'rollup-plugin-dts'
import typescript from '@rollup/plugin-typescript'
import { typescriptPaths } from 'rollup-plugin-typescript-paths'
const extensions = ['.ts', '.tsx']
const webComponentsConfig = {
const indexConfig = {
input: './src/index.ts',
output: {
file: 'dist/index.mjs',
file: 'dist/index.js',
format: 'es',
},
external: ['models', 'utils', 'react'],
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production'),
}),
resolve({ extensions }),
babel({
babelHelpers: 'bundled',
@ -44,13 +37,16 @@ const webComponentsConfig = {
],
}
const config = [
webComponentsConfig,
const configs = [
indexConfig,
{
input: './dist/dts/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [dts()],
...indexConfig,
input: './src/web.ts',
output: {
file: 'dist/web.js',
format: 'es',
},
},
]
export default config
export default configs

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,10 +1,9 @@
import { LiteBadge } from './LiteBadge'
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { getViewerUrl, injectCustomHeadCode, isEmpty, isNotEmpty } from 'utils'
import { injectCustomHeadCode, isNotEmpty } from 'utils'
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
import { ConversationContainer } from './ConversationContainer'
import css from '../assets/index.css'
import { StartParams } from 'models'
import type { ChatReply, StartParams } from 'models'
import { setIsMobile } from '@/utils/isMobileSignal'
import { BotContext, InitialChatReply } from '@/types'
import { ErrorMessage } from './ErrorMessage'
@ -20,20 +19,19 @@ export type BotProps = StartParams & {
onAnswer?: (answer: { message: string; blockId: string }) => void
onInit?: () => void
onEnd?: () => void
onNewLogs?: (logs: ChatReply['logs']) => void
}
export const Bot = (props: BotProps) => {
export const Bot = (props: BotProps & { class?: string }) => {
const [initialChatReply, setInitialChatReply] = createSignal<
InitialChatReply | undefined
>()
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 [customCss, setCustomCss] = createSignal('')
const [isInitialized, setIsInitialized] = createSignal(false)
const [error, setError] = createSignal<Error | undefined>()
const initializeBot = async () => {
setIsInitialized(true)
const urlParams = new URLSearchParams(location.search)
props.onInit?.()
const prefilledVariables: { [key: string]: string } = {}
@ -56,37 +54,51 @@ export const Bot = (props: BotProps) => {
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.'))
if (error.code === 'NOT_FOUND')
setError(new Error("The bot you're looking for doesn't exist."))
return
}
if (!data) return setError(new Error("Couldn't initiate the chat"))
if (!data) return setError(new Error("Error! Couldn't initiate the chat."))
if (data.resultId) setResultInSession(data.resultId)
setInitialChatReply(data)
setCustomCss(data.typebot.theme.customCss ?? '')
if (data.input?.id && props.onNewInputBlock)
props.onNewInputBlock({
id: data.input.id,
groupId: data.input.groupId,
})
if (data.logs) props.onNewLogs?.(data.logs)
const customHeadCode = data.typebot.settings.metadata.customHeadCode
if (customHeadCode) injectCustomHeadCode(customHeadCode)
}
onMount(() => {
createEffect(() => {
if (!props.typebot || isInitialized()) return
initializeBot().then()
})
createEffect(() => {
if (typeof props.typebot === 'string') return
setCustomCss(props.typebot.theme.customCss ?? '')
})
onCleanup(() => {
setIsInitialized(false)
})
return (
<>
<style>{css}</style>
<style>{customCss()}</style>
<Show when={error()} keyed>
{(error) => <ErrorMessage error={error} />}
</Show>
<Show when={initialChatReply()} keyed>
{(initialChatReply) => (
<BotContent
class={props.class}
initialChatReply={{
...initialChatReply,
typebot: {
@ -103,11 +115,13 @@ export const Bot = (props: BotProps) => {
}}
context={{
apiHost: props.apiHost,
isPreview: props.isPreview ?? false,
isPreview:
typeof props.typebot !== 'string' || (props.isPreview ?? false),
typebotId: initialChatReply.typebot.id,
resultId: initialChatReply.resultId,
}}
onNewInputBlock={props.onNewInputBlock}
onNewLogs={props.onNewLogs}
onAnswer={props.onAnswer}
onEnd={props.onEnd}
/>
@ -120,9 +134,11 @@ export const Bot = (props: BotProps) => {
type BotContentProps = {
initialChatReply: InitialChatReply
context: BotContext
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
class?: string
onNewInputBlock?: (block: { id: string; groupId: string }) => void
onAnswer?: (answer: { message: string; blockId: string }) => void
onEnd?: () => void
onNewLogs?: (logs: ChatReply['logs']) => void
}
const BotContent = (props: BotContentProps) => {
@ -160,7 +176,10 @@ const BotContent = (props: BotContentProps) => {
return (
<div
ref={botContainer}
class="relative flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container"
class={
'relative flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container ' +
props.class
}
>
<div class="flex w-full h-full justify-center">
<ConversationContainer
@ -169,6 +188,7 @@ const BotContent = (props: BotContentProps) => {
onNewInputBlock={props.onNewInputBlock}
onAnswer={props.onAnswer}
onEnd={props.onEnd}
onNewLogs={props.onNewLogs}
/>
</div>
<Show

View File

@ -1,6 +1,6 @@
import { createSignal, onCleanup, onMount } from 'solid-js'
import { Avatar } from '@/components/avatars/Avatar'
import { isMobile } from '@/utils/isMobileSignal'
import { Avatar } from '../avatars/Avatar'
type Props = { hostAvatarSrc?: string }
@ -42,7 +42,7 @@ export const AvatarSideContainer = (props: Props) => {
transition: 'top 350ms ease-out',
}}
>
<Avatar avatarSrc={props.hostAvatarSrc} />
<Avatar initialAvatarSrc={props.hostAvatarSrc} />
</div>
</div>
)

View File

@ -1,6 +1,6 @@
import { BotContext } from '@/types'
import { ChatReply, Settings, Theme } from 'models'
import { createSignal, For, Show } from 'solid-js'
import type { ChatReply, Settings, Theme } from 'models'
import { createSignal, For, onMount, Show } from 'solid-js'
import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock'
import { AvatarSideContainer } from './AvatarSideContainer'
@ -19,6 +19,10 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
export const ChatChunk = (props: Props) => {
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(0)
onMount(() => {
props.onScrollToBottom()
})
const displayNextMessage = () => {
setDisplayedMessageIndex(
displayedMessageIndex() === props.messages.length
@ -70,6 +74,9 @@ export const ChatChunk = (props: Props) => {
onSkip={props.onSkip}
guestAvatar={props.theme.chat.guestAvatar}
context={props.context}
isInputPrefillEnabled={
props.settings.general.isInputPrefillEnabled ?? true
}
/>
)}
</div>

View File

@ -1,5 +1,5 @@
import { ChatReply, Theme } from 'models'
import { createSignal, For } from 'solid-js'
import type { ChatReply, Theme } from 'models'
import { createEffect, createSignal, For } from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery'
import { ChatChunk } from './ChatChunk'
import { BotContext, InitialChatReply } from '@/types'
@ -7,24 +7,26 @@ import { executeIntegrations } from '@/utils/executeIntegrations'
import { executeLogic } from '@/utils/executeLogic'
const parseDynamicTheme = (
theme: Theme,
initialTheme: Theme,
dynamicTheme: ChatReply['dynamicTheme']
): Theme => ({
...theme,
...initialTheme,
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,
...initialTheme.chat,
hostAvatar:
initialTheme.chat.hostAvatar && dynamicTheme?.hostAvatarUrl
? {
...initialTheme.chat.hostAvatar,
url: dynamicTheme.hostAvatarUrl,
}
: initialTheme.chat.hostAvatar,
guestAvatar:
initialTheme.chat.guestAvatar && dynamicTheme?.guestAvatarUrl
? {
...initialTheme.chat.guestAvatar,
url: dynamicTheme?.guestAvatarUrl,
}
: initialTheme.chat.guestAvatar,
},
})
@ -34,9 +36,11 @@ type Props = {
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
onAnswer?: (answer: { message: string; blockId: string }) => void
onEnd?: () => void
onNewLogs?: (logs: ChatReply['logs']) => void
}
export const ConversationContainer = (props: Props) => {
let chatContainer: HTMLDivElement | undefined
let bottomSpacer: HTMLDivElement | undefined
const [chatChunks, setChatChunks] = createSignal<ChatReply[]>([
{
@ -44,12 +48,16 @@ export const ConversationContainer = (props: Props) => {
messages: props.initialChatReply.messages,
},
])
const [theme, setTheme] = createSignal(
parseDynamicTheme(
props.initialChatReply.typebot.theme,
props.initialChatReply.dynamicTheme
const [dynamicTheme, setDynamicTheme] = createSignal<
ChatReply['dynamicTheme']
>(props.initialChatReply.dynamicTheme)
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
createEffect(() => {
setTheme(
parseDynamicTheme(props.initialChatReply.typebot.theme, dynamicTheme())
)
)
})
const sendMessage = async (message: string) => {
const currentBlockId = chatChunks().at(-1)?.input?.id
@ -61,7 +69,8 @@ export const ConversationContainer = (props: Props) => {
message,
})
if (!data) return
if (data.dynamicTheme) applyDynamicTheme(data.dynamicTheme)
if (data.logs) props.onNewLogs?.(data.logs)
if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme)
if (data.input?.id && props.onNewInputBlock) {
props.onNewInputBlock({
id: data.input.id,
@ -83,19 +92,18 @@ 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)
chatContainer?.scrollTo(0, chatContainer.scrollHeight)
}, 50)
}
return (
<div class="overflow-y-scroll w-full min-h-full rounded px-3 pt-10 relative scrollable-container typebot-chat-view">
<div
ref={chatContainer}
class="overflow-y-scroll w-full min-h-full rounded px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth"
>
<For each={chatChunks()}>
{(chatChunk, index) => (
<ChatChunk

View File

@ -4,7 +4,7 @@ type Props = {
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>
<p class="text-2xl text-center">{props.error.message}</p>
</div>
)
}

View File

@ -1,10 +1,9 @@
import {
import type {
ChatReply,
ChoiceInputBlock,
DateInputOptions,
EmailInputBlock,
FileInputBlock,
InputBlockType,
NumberInputBlock,
PaymentInputOptions,
PhoneNumberInputBlock,
@ -14,6 +13,7 @@ import {
Theme,
UrlInputBlock,
} from 'models'
import { InputBlockType } from 'models/features/blocks/inputs/enums'
import { GuestBubble } from './bubbles/GuestBubble'
import { BotContext, InputSubmitContent } from '@/types'
import { TextInput } from '@/features/blocks/inputs/textInput'
@ -35,6 +35,7 @@ type Props = {
guestAvatar?: Theme['chat']['guestAvatar']
inputIndex: number
context: BotContext
isInputPrefillEnabled: boolean
onSubmit: (answer: string) => void
onSkip: () => void
}
@ -72,6 +73,7 @@ export const InputChatBlock = (props: Props) => {
context={props.context}
block={props.block}
inputIndex={props.inputIndex}
isInputPrefillEnabled={props.isInputPrefillEnabled}
onSubmit={handleSubmit}
onSkip={() => props.onSkip()}
hasGuestAvatar={props.guestAvatar?.isEnabled ?? false}
@ -87,17 +89,21 @@ const Input = (props: {
block: NonNullable<ChatReply['input']>
inputIndex: number
hasGuestAvatar: boolean
isInputPrefillEnabled: boolean
onSubmit: (answer: InputSubmitContent) => void
onSkip: () => void
}) => {
const onSubmit = (answer: InputSubmitContent) => props.onSubmit(answer)
const getPrefilledValue = () =>
props.isInputPrefillEnabled ? props.block.prefilledValue : undefined
return (
<Switch>
<Match when={props.block.type === InputBlockType.TEXT}>
<TextInput
block={props.block as TextInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
hasGuestAvatar={props.hasGuestAvatar}
/>
@ -105,7 +111,7 @@ const Input = (props: {
<Match when={props.block.type === InputBlockType.NUMBER}>
<NumberInput
block={props.block as NumberInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
hasGuestAvatar={props.hasGuestAvatar}
/>
@ -113,7 +119,7 @@ const Input = (props: {
<Match when={props.block.type === InputBlockType.EMAIL}>
<EmailInput
block={props.block as EmailInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
hasGuestAvatar={props.hasGuestAvatar}
/>
@ -121,7 +127,7 @@ const Input = (props: {
<Match when={props.block.type === InputBlockType.URL}>
<UrlInput
block={props.block as UrlInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
hasGuestAvatar={props.hasGuestAvatar}
/>
@ -129,7 +135,7 @@ const Input = (props: {
<Match when={props.block.type === InputBlockType.PHONE}>
<PhoneInput
block={props.block as PhoneNumberInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
hasGuestAvatar={props.hasGuestAvatar}
/>
@ -150,7 +156,7 @@ const Input = (props: {
<Match when={props.block.type === InputBlockType.RATING}>
<RatingForm
block={props.block as RatingInputBlock}
defaultValue={props.block.prefilledValue}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
/>
</Match>

View File

@ -1,25 +1,29 @@
import { isMobile } from '@/utils/isMobileSignal'
import { Show } from 'solid-js'
import { createSignal, Show } from 'solid-js'
import { isNotEmpty } from 'utils'
import { DefaultAvatar } from './DefaultAvatar'
export const Avatar = (props: { avatarSrc?: string }) => (
<Show
when={isNotEmpty(props.avatarSrc)}
keyed
fallback={() => <DefaultAvatar />}
>
<figure
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')
}
export const Avatar = (props: { initialAvatarSrc?: string }) => {
const [avatarSrc] = createSignal(props.initialAvatarSrc)
return (
<Show
when={isNotEmpty(avatarSrc())}
keyed
fallback={() => <DefaultAvatar />}
>
<img
src={props.avatarSrc}
alt="Bot avatar"
class="rounded-full object-cover w-full h-full"
/>
</figure>
</Show>
)
<figure
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={avatarSrc()}
alt="Bot avatar"
class="rounded-full object-cover w-full h-full"
/>
</figure>
</Show>
)
}

View File

@ -1,5 +1,4 @@
import { Show } from 'solid-js'
import { isDefined } from 'utils'
import { Avatar } from '../avatars/Avatar'
type Props = {
@ -19,8 +18,8 @@ export const GuestBubble = (props: Props) => (
>
{props.message}
</span>
<Show when={isDefined(props.avatarSrc)}>
<Avatar avatarSrc={props.avatarSrc} />
<Show when={props.showAvatar}>
<Avatar initialAvatarSrc={props.avatarSrc} />
</Show>
</div>
)

View File

@ -3,15 +3,15 @@ import { EmbedBubble } from '@/features/blocks/bubbles/embed'
import { ImageBubble } from '@/features/blocks/bubbles/image'
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
import { VideoBubble } from '@/features/blocks/bubbles/video'
import {
import type {
AudioBubbleContent,
BubbleBlockType,
ChatMessage,
EmbedBubbleContent,
ImageBubbleContent,
TextBubbleContent,
VideoBubbleContent,
} from 'models'
import { BubbleBlockType } from 'models/features/blocks/bubbles/enums'
import { Match, Switch } from 'solid-js'
type Props = {

View File

@ -0,0 +1,3 @@
export * from './SendButton'
export * from './TypingBubble'
export * from './inputs'

View File

@ -1,4 +1,6 @@
import type { BotProps, PopupProps, BubbleProps } from '@typebot.io/js'
import type { BubbleProps } from './features/bubble'
import type { PopupProps } from './features/popup'
import type { BotProps } from './components/Bot'
export const defaultBotProps: BotProps = {
typebot: '',
@ -6,6 +8,7 @@ export const defaultBotProps: BotProps = {
onAnswer: undefined,
onEnd: undefined,
onInit: undefined,
onNewLogs: undefined,
isPreview: undefined,
startGroupId: undefined,
prefilledVariables: undefined,

View File

@ -1,8 +0,0 @@
import { Bot } from '@/components/Bot'
import type { Component } from 'solid-js'
export const App: Component = () => {
return (
<Bot typebot="clbm11cku000t3b6o01ug8awh" apiHost="http://localhost:3001" />
)
}

View File

@ -1,5 +0,0 @@
import { render } from 'solid-js/web'
import { App } from './App'
import '../assets/index.css'
render(() => <App />, document.getElementById('root') as HTMLElement)

9
packages/js/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
export {}
declare module 'solid-js' {
namespace JSX {
interface CustomEvents {
click: MouseEvent
}
}
}

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { AudioBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { AudioBubbleContent } from 'models'
import { createSignal, onCleanup, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { EmbedBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { EmbedBubbleContent } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { ImageBubbleContent } from 'models'
import { TypingBubble } from '@/components'
import type { ImageBubbleContent } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {

View File

@ -1,5 +1,5 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { TextBubbleContent, TypingEmulation } from 'models'
import { TypingBubble } from '@/components'
import type { TextBubbleContent, TypingEmulation } from 'models'
import { createSignal, onMount } from 'solid-js'
import { computeTypingDuration } from '../utils/computeTypingDuration'

View File

@ -1,4 +1,4 @@
import { TypingEmulation } from 'models'
import type { TypingEmulation } from 'models'
export const computeTypingDuration = (
bubbleContent: string,

View File

@ -1,5 +1,6 @@
import { TypingBubble } from '@/components/bubbles/TypingBubble'
import { VideoBubbleContent, VideoBubbleContentType } from 'models'
import { TypingBubble } from '@/components'
import type { VideoBubbleContent } from 'models'
import { VideoBubbleContentType } from 'models/features/blocks/bubbles/video/enums'
import { createSignal, Match, onMount, Switch } from 'solid-js'
type Props = {
@ -64,10 +65,7 @@ const VideoContent = (props: VideoContentProps) => {
<Match
when={
props.content?.type &&
[
VideoBubbleContentType.VIMEO,
VideoBubbleContentType.YOUTUBE,
].includes(props.content.type)
props.content.type === VideoBubbleContentType.URL
}
>
<video

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { ChoiceInputBlock } from 'models'
import type { ChoiceInputBlock } from 'models'
import { createSignal, For } from 'solid-js'
type Props = {
@ -12,8 +12,7 @@ type Props = {
export const ChoiceForm = (props: Props) => {
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
const handleClick = (itemIndex: number) => (e: MouseEvent) => {
e.preventDefault()
const handleClick = (itemIndex: number) => {
if (props.block.options?.isMultipleChoice)
toggleSelectedItemIndex(itemIndex)
else props.onSubmit({ value: props.block.items[itemIndex].content ?? '' })
@ -47,7 +46,8 @@ export const ChoiceForm = (props: Props) => {
role={
props.block.options?.isMultipleChoice ? 'checkbox' : 'button'
}
onClick={(event) => handleClick(index())(event)}
type="button"
on:click={() => handleClick(index())}
class={
'py-2 px-4 text-left font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
(selectedIndices().some(

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { DateInputOptions } from 'models'
import type { DateInputOptions } from 'models'
import { createSignal } from 'solid-js'
import { parseReadableDate } from '../utils/parseReadableDate'

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { EmailInputBlock } from 'models'
import type { EmailInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -59,7 +59,7 @@ export const EmailInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,6 +1,7 @@
import { SendButton, Spinner } from '@/components/SendButton'
import { BotContext, InputSubmitContent } from '@/types'
import { defaultFileInputOptions, FileInputBlock } from 'models'
import { FileInputBlock } from 'models'
import { defaultFileInputOptions } from 'models/features/blocks/inputs/file'
import { createSignal, Match, Show, Switch } from 'solid-js'
import { uploadFiles } from 'utils'
@ -140,7 +141,7 @@ export const FileUploadForm = (props: Props) => {
<span class="relative">
<FileIcon />
<div
class="total-files-indicator flex items-center justify-center absolute -right-1 rounded-full px-1 h-4"
class="total-files-indicator flex items-center justify-center absolute -right-1 rounded-full px-1 w-4 h-4"
style={{ bottom: '5px' }}
>
{selectedFiles().length}
@ -177,7 +178,7 @@ export const FileUploadForm = (props: Props) => {
class={
'py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button '
}
onClick={() => props.onSkip()}
on:click={() => props.onSkip()}
>
{props.block.options.labels.skip ??
defaultFileInputOptions.labels.skip}
@ -198,7 +199,7 @@ export const FileUploadForm = (props: Props) => {
class={
'secondary-button py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 mr-2'
}
onClick={clearFiles}
on:click={clearFiles}
>
{props.block.options.labels.clear ??
defaultFileInputOptions.labels.clear}
@ -233,7 +234,7 @@ const UploadIcon = () => (
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-3"
class="mb-3 text-gray-500"
>
<polyline points="16 16 12 12 8 16" />
<line x1="12" y1="12" x2="12" y2="21" />
@ -244,7 +245,6 @@ const UploadIcon = () => (
const FileIcon = () => (
<svg
class="mb-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
@ -254,6 +254,7 @@ const FileIcon = () => (
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-3 text-gray-500"
>
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
<polyline points="13 2 13 9 20 9" />

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { NumberInputBlock } from 'models'
import type { NumberInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type NumberInputProps = {
@ -62,7 +62,7 @@ export const NumberInput = (props: NumberInputProps) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,5 +1,6 @@
import { BotContext } from '@/types'
import { PaymentInputOptions, PaymentProvider, RuntimeOptions } from 'models'
import type { PaymentInputOptions, RuntimeOptions } from 'models'
import { PaymentProvider } from 'models/features/blocks/inputs/payment/enums'
import { Match, Switch } from 'solid-js'
import { StripePaymentForm } from './StripePaymentForm'

View File

@ -1,9 +1,9 @@
import { SendButton } from '@/components/SendButton'
import { createSignal, onMount, Show } from 'solid-js'
import { loadStripe } from '@stripe/stripe-js/pure'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import { BotContext } from '@/types'
import { PaymentInputOptions, RuntimeOptions } from 'models'
import type { PaymentInputOptions, RuntimeOptions } from 'models'
import { loadStripe } from '@/lib/stripe'
type Props = {
context: BotContext

View File

@ -1,4 +1,4 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
@ -99,7 +99,7 @@ export const PhoneInput = (props: PhoneInputProps) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,6 +1,6 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { RatingInputBlock, RatingInputOptions } from 'models'
import type { RatingInputBlock, RatingInputOptions } from 'models'
import { createSignal, For, Match, Switch } from 'solid-js'
import { isDefined, isEmpty, isNotDefined } from 'utils'
@ -84,7 +84,7 @@ const RatingButton = (props: RatingButtonProps) => {
<Switch>
<Match when={props.buttonType === 'Numbers'}>
<button
onClick={(e) => {
on:click={(e) => {
e.preventDefault()
props.onClick(props.idx)
}}
@ -111,7 +111,7 @@ const RatingButton = (props: RatingButtonProps) => {
? props.customIcon.svg
: defaultIcon
}
onClick={() => props.onClick(props.idx)}
on:click={() => props.onClick(props.idx)}
/>
</Match>
</Switch>

View File

@ -1,8 +1,8 @@
import { Textarea, ShortTextInput } from '@/components/inputs'
import { Textarea, ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { TextInputBlock } from 'models'
import type { TextInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -69,7 +69,7 @@ export const TextInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,8 +1,8 @@
import { ShortTextInput } from '@/components/inputs'
import { ShortTextInput } from '@/components'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import { UrlInputBlock } from 'models'
import type { UrlInputBlock } from 'models'
import { createSignal, onMount } from 'solid-js'
type Props = {
@ -65,7 +65,7 @@ export const UrlInput = (props: Props) => {
type="button"
isDisabled={inputValue() === ''}
class="my-2 ml-2"
onClick={submit}
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
</SendButton>

View File

@ -1,5 +1,5 @@
import { executeCode } from '@/features/blocks/logic/code'
import { CodeToExecute } from 'models'
import type { CodeToExecute } from 'models'
export const executeChatwoot = (chatwoot: { codeToExecute: CodeToExecute }) => {
executeCode(chatwoot.codeToExecute)

View File

@ -1,5 +1,5 @@
import { sendGaEvent } from '@/lib/gtag'
import { GoogleAnalyticsOptions } from 'models'
import type { GoogleAnalyticsOptions } from 'models'
export const executeGoogleAnalyticsBlock = async (
options: GoogleAnalyticsOptions

View File

@ -1,4 +1,4 @@
import { CodeToExecute } from 'models'
import type { CodeToExecute } from 'models'
export const executeCode = async ({ content, args }: CodeToExecute) => {
const func = Function(...args.map((arg) => arg.id), content)

View File

@ -1,4 +1,4 @@
import { RedirectOptions } from 'models'
import type { RedirectOptions } from 'models'
export const executeRedirect = ({ url, isNewTab }: RedirectOptions) => {
if (!url) return

View File

@ -1,11 +1,11 @@
import styles from '../../../assets/index.css'
import { createSignal, onMount, Show, splitProps, onCleanup } from 'solid-js'
import { Bot, BotProps } from '../../../components/Bot'
import { CommandData } from '@/features/commands'
import styles from '../../../assets/index.css'
import { CommandData } from '../../commands'
import { BubbleButton } from './BubbleButton'
import { PreviewMessage, PreviewMessageProps } from './PreviewMessage'
import { isDefined } from 'utils'
import { BubbleParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
export type BubbleProps = BotProps &
BubbleParams & {
@ -131,7 +131,11 @@ export const Bubble = (props: BubbleProps) => {
}
>
<Show when={isBotStarted()}>
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
<Bot
{...botProps}
prefilledVariables={prefilledVariables()}
class="rounded-lg"
/>
</Show>
</div>
</>

View File

@ -1,4 +1,5 @@
import { Show } from 'solid-js'
import { isNotDefined } from 'utils'
import { ButtonTheme } from '../types'
type Props = ButtonTheme & {
@ -7,6 +8,7 @@ type Props = ButtonTheme & {
}
const defaultButtonColor = '#0042DA'
const defaultIconColor = 'white'
export const BubbleButton = (props: Props) => {
return (
@ -20,27 +22,23 @@ export const BubbleButton = (props: Props) => {
'background-color': props.backgroundColor ?? defaultButtonColor,
}}
>
<Show when={props.icon?.color} keyed>
{(color) => (
<svg
viewBox="0 0 24 24"
style={{
stroke: color,
}}
class={
`w-7 stroke-2 fill-transparent absolute duration-200 transition ` +
(props.isBotOpened
? 'scale-0 opacity-0'
: 'scale-100 opacity-100')
}
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
)}
<Show when={isNotDefined(props.customIconSrc)} keyed>
<svg
viewBox="0 0 24 24"
style={{
stroke: props.iconColor ?? defaultIconColor,
}}
class={
`w-7 stroke-2 fill-transparent absolute duration-200 transition ` +
(props.isBotOpened ? 'scale-0 opacity-0' : 'scale-100 opacity-100')
}
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
</Show>
<Show when={props.icon?.url}>
<Show when={props.customIconSrc}>
<img
src={props.icon?.url}
src={props.customIconSrc}
class="w-7 h-7 rounded-full object-cover"
alt="Bubble button icon"
/>
@ -48,7 +46,7 @@ export const BubbleButton = (props: Props) => {
<svg
viewBox="0 0 24 24"
style={{ fill: props.icon?.color ?? 'white' }}
style={{ fill: props.iconColor ?? 'white' }}
class={
`w-7 absolute duration-200 transition ` +
(props.isBotOpened

View File

@ -10,8 +10,8 @@ export type PreviewMessageProps = Pick<
onCloseClick: () => void
}
const defaultFontFamily =
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
const defaultBackgroundColor = '#F7F8FF'
const defaultTextColor = '#303235'
export const PreviewMessage = (props: PreviewMessageProps) => {
const [isPreviewMessageHovered, setIsPreviewMessageHovered] =
@ -23,11 +23,9 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
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"
style={{
'font-family':
props.previewMessageTheme?.fontFamily ?? defaultFontFamily,
'background-color':
props.previewMessageTheme?.backgroundColor ?? '#F7F8FF',
color: props.previewMessageTheme?.color ?? '#303235',
props.previewMessageTheme?.backgroundColor ?? defaultBackgroundColor,
color: props.previewMessageTheme?.textColor ?? defaultTextColor,
}}
onMouseEnter={() => setIsPreviewMessageHovered(true)}
onMouseLeave={() => setIsPreviewMessageHovered(false)}
@ -43,8 +41,10 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
}}
style={{
'background-color':
props.previewMessageTheme?.closeButtonBgColor ?? '#F7F8FF',
color: props.previewMessageTheme?.closeButtonColor ?? '#303235',
props.previewMessageTheme?.closeButtonBackgroundColor ??
defaultBackgroundColor,
color:
props.previewMessageTheme?.closeButtonIconColor ?? defaultTextColor,
}}
>
<svg

View File

@ -10,10 +10,8 @@ export type BubbleTheme = {
export type ButtonTheme = {
backgroundColor?: string
icon?: {
color?: string
url?: string
}
iconColor?: string
customIconSrc?: string
}
export type PreviewMessageParams = {
@ -24,8 +22,7 @@ export type PreviewMessageParams = {
export type PreviewMessageTheme = {
backgroundColor?: string
color?: string
fontFamily?: string
closeButtonBgColor?: string
closeButtonColor?: string
textColor?: string
closeButtonBackgroundColor?: string
closeButtonIconColor?: string
}

View File

@ -7,10 +7,10 @@ import {
onCleanup,
createEffect,
} from 'solid-js'
import { Bot, BotProps } from '../../../components/Bot'
import { CommandData } from '@/features/commands'
import { isDefined } from 'utils'
import { CommandData } from '../../commands'
import { isDefined, isNotDefined } from 'utils'
import { PopupParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
export type PopupProps = BotProps &
PopupParams & {
@ -43,8 +43,6 @@ export const Popup = (props: PopupProps) => {
)
onMount(() => {
document.addEventListener('pointerdown', processWindowClick)
botContainer?.addEventListener('pointerdown', stopPropagation)
window.addEventListener('message', processIncomingEvent)
const autoShowDelay = popupProps.autoShowDelay
if (isDefined(autoShowDelay)) {
@ -54,20 +52,14 @@ export const Popup = (props: PopupProps) => {
}
})
createEffect(() => {
const isOpen = popupProps.isOpen
if (isDefined(isOpen)) setIsBotOpened(isOpen)
})
onCleanup(() => {
document.removeEventListener('pointerdown', processWindowClick)
botContainer?.removeEventListener('pointerdown', stopPropagation)
window.removeEventListener('message', processIncomingEvent)
})
const processWindowClick = () => {
setIsBotOpened(false)
}
createEffect(() => {
if (isNotDefined(props.isOpen) || props.isOpen === isBotOpened()) return
toggleBot()
})
const stopPropagation = (event: MouseEvent) => {
event.stopPropagation()
@ -87,24 +79,28 @@ export const Popup = (props: PopupProps) => {
}
const openBot = () => {
if (isBotOpened()) popupProps.onOpen?.()
if (isDefined(props.isOpen)) return
setIsBotOpened(true)
popupProps.onOpen?.()
document.body.style.overflow = 'hidden'
document.addEventListener('pointerdown', closeBot)
botContainer?.addEventListener('pointerdown', stopPropagation)
}
const closeBot = () => {
if (isBotOpened()) popupProps.onClose?.()
if (isDefined(props.isOpen)) return
setIsBotOpened(false)
popupProps.onClose?.()
document.body.style.overflow = 'auto'
document.removeEventListener('pointerdown', closeBot)
botContainer?.removeEventListener('pointerdown', stopPropagation)
}
const toggleBot = () => {
if (isDefined(props.isOpen)) return
isBotOpened() ? closeBot() : openBot()
}
return (
<Show when={isBotOpened()}>
<style>{styles}</style>
<div
class="relative z-10"
aria-labelledby="modal-title"

View File

@ -2,6 +2,5 @@ export type PopupParams = {
autoShowDelay?: number
theme?: {
width?: string
backgroundColor?: string
}
}

View File

@ -0,0 +1,47 @@
import styles from '../../../assets/index.css'
import { Bot, BotProps } from '@/components/Bot'
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
const hostElementCss = `
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: hidden;
}
`
export const Standard = (props: BotProps) => {
const [isBotDisplayed, setIsBotDisplayed] = createSignal(false)
const launchBot = () => {
setIsBotDisplayed(true)
}
const observer = new IntersectionObserver((intersections) => {
if (intersections.some((intersection) => intersection.isIntersecting))
launchBot()
})
onMount(() => {
const standardElement = document.querySelector('typebot-standard')
if (!standardElement) return
observer.observe(standardElement)
})
onCleanup(() => {
observer.disconnect()
})
return (
<>
<style>
{styles}
{hostElementCss}
</style>
<Show when={isBotDisplayed()}>
<Bot {...props} />
</Show>
</>
)
}

View File

@ -0,0 +1 @@
export * from './Standard'

View File

@ -0,0 +1 @@
export * from './components'

View File

@ -1,5 +1,5 @@
export * from './register'
export type { BotProps } from './components/Bot'
export type { BubbleProps } from './features/bubble'
export type { PopupProps } from './features/popup'
export * from './features/commands'
export type { BotProps } from './components/Bot'
export type { PopupProps } from './features/popup/components/Popup'
export type { BubbleProps } from './features/bubble/components/Bubble'

View File

@ -1,4 +1,4 @@
import { GoogleAnalyticsOptions } from 'models'
import type { GoogleAnalyticsOptions } from 'models'
declare const gtag: (
type: string,

View File

@ -0,0 +1,13 @@
import { Stripe } from '@stripe/stripe-js'
export const loadStripe = (publishableKey: string): Promise<Stripe> =>
new Promise<Stripe>((resolve) => {
if (window.Stripe) return resolve(window.Stripe(publishableKey))
const script = document.createElement('script')
script.src = 'https://js.stripe.com/v3'
document.body.appendChild(script)
script.onload = () => {
if (!window.Stripe) throw new Error('Stripe.js failed to load.')
resolve(window.Stripe(publishableKey))
}
})

View File

@ -1,6 +1,7 @@
import { InitialChatReply } from '@/types'
import { SendMessageInput, StartParams } from 'models'
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
import { guessApiHost } from '@/utils/guessApiHost'
import type { SendMessageInput, StartParams } from 'models'
import { isNotEmpty, sendRequest } from 'utils'
export async function getInitialChatReplyQuery({
typebot,
@ -17,7 +18,7 @@ export async function getInitialChatReplyQuery({
return sendRequest<InitialChatReply>({
method: 'POST',
url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sendMessage`,
body: {
startParams: {
isPreview,

View File

@ -1,4 +1,4 @@
import { ChatReply, SendMessageInput } from 'models'
import type { ChatReply, SendMessageInput } from 'models'
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
export async function sendMessageQuery({

View File

@ -1,19 +1,16 @@
import { customElement } from 'solid-element'
import { Bot, BotProps } from './components/Bot'
import { Bubble, BubbleProps } from './features/bubble'
import { Popup, PopupProps } from './features/popup'
import {
defaultBotProps,
defaultBubbleProps,
defaultPopupProps,
} from './constants'
import { Bubble } from './features/bubble'
import { Popup } from './features/popup'
import { Standard } from './features/standard'
export const registerStandardComponent = (props: BotProps) => {
export const registerWebComponents = () => {
if (typeof window === 'undefined') return
customElement('typebot-standard', props, Bot)
}
export const registerBubbleComponent = (props: BubbleProps) => {
if (typeof window === 'undefined') return
customElement('typebot-bubble', props, Bubble)
}
export const registerPopupComponent = (props: PopupProps) => {
if (typeof window === 'undefined') return
customElement('typebot-popup', props, Popup)
customElement('typebot-standard', defaultBotProps, Standard)
customElement('typebot-bubble', defaultBubbleProps, Bubble)
customElement('typebot-popup', defaultPopupProps, Popup)
}

View File

@ -1,4 +1,4 @@
import { ChatReply } from 'models'
import type { ChatReply } from 'models'
export type InputSubmitContent = {
label?: string

View File

@ -1,6 +1,6 @@
import { executeChatwoot } from '@/features/blocks/integrations/chatwoot'
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics'
import { ChatReply } from 'models'
import type { ChatReply } from 'models'
export const executeIntegrations = async (
integrations: ChatReply['integrations']

View File

@ -1,6 +1,6 @@
import { executeCode } from '@/features/blocks/logic/code'
import { executeRedirect } from '@/features/blocks/logic/redirect'
import { ChatReply } from 'models'
import type { ChatReply } from 'models'
export const executeLogic = async (logic: ChatReply['logic']) => {
if (logic?.codeToExecute) {

View File

@ -0,0 +1,6 @@
import { env } from 'utils'
const cloudViewerUrl = 'https://viewer.typebot.io'
export const guessApiHost = () =>
env('VIEWER_URL')?.split(',')[0] ?? cloudViewerUrl

View File

@ -1,12 +1,12 @@
import {
Background,
BackgroundType,
ChatTheme,
ContainerColors,
GeneralTheme,
InputColors,
Theme,
} from 'models'
import { BackgroundType } from 'models/features/typebot/theme/enums'
const cssVariableNames = {
general: {

5
packages/js/src/web.ts Normal file
View File

@ -0,0 +1,5 @@
import { registerWebComponents } from './register'
import { injectTypebotInWindow } from './window'
registerWebComponents()
injectTypebotInWindow()

65
packages/js/src/window.ts Normal file
View File

@ -0,0 +1,65 @@
import { BubbleProps } from './features/bubble'
import { PopupProps } from './features/popup'
import { BotProps } from './components/Bot'
import {
close,
hidePreviewMessage,
open,
setPrefilledVariables,
showPreviewMessage,
toggle,
} from './features/commands'
export const initStandard = (
props: BotProps & { style?: string; class?: string }
) => {
const standardElement = document.querySelector('typebot-standard')
if (!standardElement) throw new Error('<typebot-standard> element not found.')
Object.assign(standardElement, props)
}
export const initPopup = (props: PopupProps) => {
const popupElement = document.createElement('typebot-popup')
Object.assign(popupElement, props)
document.body.appendChild(popupElement)
}
export const initBubble = (props: BubbleProps) => {
const bubbleElement = document.createElement('typebot-bubble')
Object.assign(bubbleElement, props)
document.body.appendChild(bubbleElement)
}
declare const window:
| {
Typebot:
| {
initStandard: typeof initStandard
initPopup: typeof initPopup
initBubble: typeof initBubble
close: typeof close
hidePreviewMessage: typeof hidePreviewMessage
open: typeof open
setPrefilledVariables: typeof setPrefilledVariables
showPreviewMessage: typeof showPreviewMessage
toggle: typeof toggle
}
| undefined
}
| undefined
export const injectTypebotInWindow = () => {
if (typeof window === 'undefined') return
window.Typebot = {
initStandard,
initPopup,
initBubble,
close,
hidePreviewMessage,
open,
setPrefilledVariables,
showPreviewMessage,
toggle,
}
}

View File

@ -12,7 +12,8 @@
"module": "ESNext",
"target": "ESNext",
"declaration": true,
"declarationDir": "dts",
"declarationMap": true,
"outDir": "dist",
"emitDeclarationOnly": true
}
}

View File

@ -1,21 +0,0 @@
import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'
import { resolve } from 'path'
export default defineConfig({
plugins: [solidPlugin()],
server: {
port: 3005,
},
build: {
target: 'esnext',
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
optimizeDeps: {
include: ['models', 'utils'],
},
})

View File

@ -0,0 +1,11 @@
import { z } from 'zod'
export const blockBaseSchema = z.object({
id: z.string(),
groupId: z.string(),
outgoingEdgeId: z.string().optional(),
})
export const optionBaseSchema = z.object({
variableId: z.string().optional(),
})

View File

@ -1,5 +1,6 @@
import { z } from 'zod'
import { blockBaseSchema, BubbleBlockType } from '../shared'
import { blockBaseSchema } from '../baseSchemas'
import { BubbleBlockType } from './enums'
export const audioBubbleContentSchema = z.object({
url: z.string().optional(),

View File

@ -1,5 +1,6 @@
import { blockBaseSchema, BubbleBlockType } from '../shared'
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
import { BubbleBlockType } from './enums'
export const embedBubbleContentSchema = z.object({
url: z.string().optional(),

View File

@ -0,0 +1,7 @@
export enum BubbleBlockType {
TEXT = 'text',
IMAGE = 'image',
VIDEO = 'video',
EMBED = 'embed',
AUDIO = 'audio',
}

View File

@ -1,5 +1,6 @@
import { z } from 'zod'
import { blockBaseSchema, BubbleBlockType } from '../shared'
import { blockBaseSchema } from '../baseSchemas'
import { BubbleBlockType } from './enums'
export const imageBubbleContentSchema = z.object({
url: z.string().optional(),

View File

@ -1,6 +1,7 @@
export * from './bubbleBlock'
export * from './text'
export * from './image'
export * from './video'
export * from './embed'
export * from './audio'
export * from './embed'
export * from './enums'
export * from './image'
export * from './schemas'
export * from './text'
export * from './video'

View File

@ -1,5 +1,6 @@
import { blockBaseSchema, BubbleBlockType } from '../shared'
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
import { BubbleBlockType } from './enums'
export const defaultTextBubbleContent: TextBubbleContent = {
html: '',

View File

@ -0,0 +1,5 @@
export enum VideoBubbleContentType {
URL = 'url',
YOUTUBE = 'youtube',
VIMEO = 'vimeo',
}

View File

@ -0,0 +1,2 @@
export * from './enums'
export * from './schemas'

View File

@ -1,11 +1,7 @@
import { blockBaseSchema, BubbleBlockType } from '../shared'
import { z } from 'zod'
export enum VideoBubbleContentType {
URL = 'url',
YOUTUBE = 'youtube',
VIMEO = 'vimeo',
}
import { blockBaseSchema } from '../../baseSchemas'
import { BubbleBlockType } from '../enums'
import { VideoBubbleContentType } from './enums'
export const videoBubbleContentSchema = z.object({
url: z.string().optional(),

View File

@ -1,7 +1,7 @@
export * from './blocks'
export * from './baseSchemas'
export * from './bubbles'
export * from './inputs'
export * from './logic'
export * from './integrations'
export * from './item'
export * from './shared'
export * from './logic'
export * from './schemas'
export * from './start'

View File

@ -1,12 +1,9 @@
import { z } from 'zod'
import {
blockBaseSchema,
InputBlockType,
defaultButtonLabel,
optionBaseSchema,
itemBaseSchema,
ItemType,
} from '../shared'
import { ItemType } from '../../items/enums'
import { itemBaseSchema } from '../../items/baseSchemas'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
export const choiceInputOptionsSchema = optionBaseSchema.and(
z.object({

View File

@ -0,0 +1 @@
export const defaultButtonLabel = 'Send'

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
blockBaseSchema,
InputBlockType,
defaultButtonLabel,
optionBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
export const dateInputOptionsSchema = optionBaseSchema.and(
z.object({

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
import { textInputOptionsBaseSchema } from './text'
export const emailInputOptionsSchema = optionBaseSchema

View File

@ -0,0 +1,12 @@
export enum InputBlockType {
TEXT = 'text input',
NUMBER = 'number input',
EMAIL = 'email input',
URL = 'url input',
DATE = 'date input',
PHONE = 'phone number input',
CHOICE = 'choice input',
PAYMENT = 'payment input',
RATING = 'rating input',
FILE = 'file input',
}

View File

@ -1,5 +1,6 @@
import { z } from 'zod'
import { InputBlockType, optionBaseSchema, blockBaseSchema } from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { InputBlockType } from './enums'
export const fileInputOptionsSchema = optionBaseSchema.and(
z.object({

View File

@ -1,11 +1,13 @@
export * from './inputBlock'
export * from './text'
export * from './email'
export * from './number'
export * from './url'
export * from './date'
export * from './choice'
export * from './constants'
export * from './date'
export * from './email'
export * from './enums'
export * from './file'
export * from './number'
export * from './payment'
export * from './phone'
export * from './rating'
export * from './file'
export * from './schemas'
export * from './text'
export * from './url'

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
import { textInputOptionsBaseSchema } from './text'
export const numberInputOptionsSchema = optionBaseSchema

View File

@ -0,0 +1,3 @@
export enum PaymentProvider {
STRIPE = 'Stripe',
}

View File

@ -0,0 +1,2 @@
export * from './enums'
export * from './schemas'

View File

@ -1,5 +1,7 @@
import { z } from 'zod'
import { InputBlockType, optionBaseSchema, blockBaseSchema } from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../../baseSchemas'
import { InputBlockType } from '../enums'
import { PaymentProvider } from './enums'
export type CreditCardDetails = {
number: string
@ -8,10 +10,6 @@ export type CreditCardDetails = {
cvc: string
}
export enum PaymentProvider {
STRIPE = 'Stripe',
}
export const paymentInputOptionsSchema = optionBaseSchema.and(
z.object({
provider: z.nativeEnum(PaymentProvider),

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
import { textInputOptionsBaseSchema } from './text'
export const phoneNumberInputOptionsSchema = optionBaseSchema

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
export const defaultRatingInputOptions: RatingInputOptions = {
buttonType: 'Numbers',

View File

@ -1,5 +1,4 @@
import { z } from 'zod'
import { optionBaseSchema } from '../shared'
import { choiceInputOptionsSchema, choiceInputSchema } from './choice'
import { dateInputOptionsSchema, dateInputSchema } from './date'
import { emailInputOptionsSchema, emailInputSchema } from './email'
@ -13,6 +12,7 @@ import { ratingInputOptionsSchema, ratingInputBlockSchema } from './rating'
import { textInputOptionsSchema, textInputSchema } from './text'
import { fileInputOptionsSchema, fileInputStepSchema } from './file'
import { urlInputOptionsSchema, urlInputSchema } from './url'
import { optionBaseSchema } from '../baseSchemas'
export type OptionBase = z.infer<typeof optionBaseSchema>

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { blockBaseSchema, optionBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
export const textInputOptionsBaseSchema = z.object({
labels: z.object({

View File

@ -1,10 +1,7 @@
import { z } from 'zod'
import {
defaultButtonLabel,
InputBlockType,
optionBaseSchema,
blockBaseSchema,
} from '../shared'
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
import { defaultButtonLabel } from './constants'
import { InputBlockType } from './enums'
import { textInputOptionsBaseSchema } from './text'
export const urlInputOptionsSchema = optionBaseSchema

View File

@ -1,5 +1,6 @@
import { z } from 'zod'
import { blockBaseSchema, IntegrationBlockType } from '../shared'
import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums'
export const chatwootOptionsSchema = z.object({
baseUrl: z.string(),

View File

@ -0,0 +1,10 @@
export enum IntegrationBlockType {
GOOGLE_SHEETS = 'Google Sheets',
GOOGLE_ANALYTICS = 'Google Analytics',
WEBHOOK = 'Webhook',
EMAIL = 'Email',
ZAPIER = 'Zapier',
MAKE_COM = 'Make.com',
PABBLY_CONNECT = 'Pabbly',
CHATWOOT = 'Chatwoot',
}

View File

@ -1,5 +1,6 @@
import { z } from 'zod'
import { blockBaseSchema, IntegrationBlockType } from '../shared'
import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums'
export const googleAnalyticsOptionsSchema = z.object({
trackingId: z.string().optional(),

View File

@ -0,0 +1,5 @@
export enum GoogleSheetsAction {
GET = 'Get data from sheet',
INSERT_ROW = 'Insert a row',
UPDATE_ROW = 'Update a row',
}

Some files were not shown because too many files have changed in this diff Show More