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

@ -11,7 +11,8 @@ import {
import { AlertIcon } from '@/components/icons'
import { Plan, Workspace } from 'db'
import React from 'react'
import { getChatsLimit, getStorageLimit, parseNumberWithCommas } from 'utils'
import { parseNumberWithCommas } from 'utils'
import { getChatsLimit, getStorageLimit } from 'utils/pricing'
import { storageToReadable } from './helpers'
import { useUsage } from '../../../hooks/useUsage'

View File

@ -18,15 +18,15 @@ import { ChevronLeftIcon } from '@/components/icons'
import { useWorkspace } from '@/features/workspace'
import { Plan } from 'db'
import { useEffect, useState } from 'react'
import { parseNumberWithCommas } from 'utils'
import {
chatsLimit,
computePrice,
formatPrice,
getChatsLimit,
getStorageLimit,
storageLimit,
parseNumberWithCommas,
formatPrice,
computePrice,
} from 'utils'
} from 'utils/pricing'
import { FeaturesList } from './components/FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'

View File

@ -14,15 +14,15 @@ import { ChevronLeftIcon } from '@/components/icons'
import { useWorkspace } from '@/features/workspace'
import { Plan } from 'db'
import { useEffect, useState } from 'react'
import { parseNumberWithCommas } from 'utils'
import {
chatsLimit,
computePrice,
formatPrice,
getChatsLimit,
getStorageLimit,
storageLimit,
parseNumberWithCommas,
computePrice,
formatPrice,
} from 'utils'
} from 'utils/pricing'
import { FeaturesList } from './components/FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'

View File

@ -1,12 +1,7 @@
import { loadStripe } from '@stripe/stripe-js/pure'
import { Plan, User } from 'db'
import {
env,
guessIfUserIsEuropean,
isDefined,
isEmpty,
sendRequest,
} from 'utils'
import { env, isDefined, isEmpty, sendRequest } from 'utils'
import { guessIfUserIsEuropean } from 'utils/pricing'
type UpgradeProps = {
user: User

View File

@ -7,9 +7,7 @@ export const executeWebhook = (
{ blockId }: { blockId: string }
) =>
sendRequest<WebhookResponse>({
url: `${getViewerUrl({
isBuilder: true,
})}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`,
url: `${getViewerUrl()}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`,
method: 'POST',
body: {
variables,

View File

@ -118,8 +118,7 @@ export const CustomDomainModal = ({
<Stack>
<Text fontWeight="bold">Value</Text>
<Text>
{env('VIEWER_INTERNAL_URL') ??
getViewerUrl({ isBuilder: true })}
{env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}
</Text>
</Stack>
</HStack>

View File

@ -123,7 +123,7 @@ export const OnboardingModal = ({ totalTypebots }: Props) => {
<ModalBody p="10">
{typebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
apiHost={getViewerUrl()}
typebot={parseTypebotToPublicTypebot(typebot)}
predefinedVariables={{
Name: user?.name?.split(' ')[0] ?? undefined,

View File

@ -106,7 +106,7 @@ export const PreviewDrawer = () => {
pointerEvents={isResizing ? 'none' : 'auto'}
>
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
apiHost={getViewerUrl()}
typebot={publicTypebot}
onNewGroupVisible={setPreviewingEdge}
onNewLog={handleNewLog}

View File

@ -98,9 +98,7 @@ export const SharePage = () => {
</Heading>
{typebot && (
<EditableUrl
hostname={
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
}
hostname={getViewerUrl() ?? 'https://typebot.io'}
pathname={publicId}
isValid={checkIfPublicIdIsValid}
onPathnameChange={handlePublicIdChange}

View File

@ -20,9 +20,9 @@ export const ChatEmbedCode = ({
const snippet = prettier.format(
createSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
button,
proactiveMessage,
}),

View File

@ -22,9 +22,9 @@ export const ContainerEmbedCode = ({
const snippet = prettier.format(
parseSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
heightLabel,
widthLabel,
}),

View File

@ -13,9 +13,9 @@ export const IframeEmbedCode = ({
heightLabel,
}: Props & FlexProps) => {
const { typebot } = useTypebot()
const src = `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`
const src = `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`
const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" style="border: none"></iframe>`
return <CodeEditor value={code} lang="html" isReadOnly />

View File

@ -17,9 +17,9 @@ export const PopupEmbedCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const { typebot } = useTypebot()
const snippet = prettier.format(
createSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
delay,
}),
{

View File

@ -20,9 +20,9 @@ export const StandardReactDiv = ({
const { typebot } = useTypebot()
const snippet = prettier.format(
parseContainerSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
heightLabel,
widthLabel,
}),
@ -70,9 +70,9 @@ export const PopupReactCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const { typebot } = useTypebot()
const snippet = prettier.format(
parsePopupSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
delay,
}),
{
@ -119,9 +119,9 @@ export const ChatReactCode = ({
const { typebot } = useTypebot()
const snippet = prettier.format(
parseBubbleSnippet({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${typebot?.publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`,
button,
proactiveMessage,
}),

View File

@ -40,9 +40,7 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
})
const jsCode = parseInitContainerCode({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${publicId}`,
})
const headCode = `${typebotJsHtml}
<script>

View File

@ -48,15 +48,13 @@ export const NotionModal = ({
pr="4.5rem"
type={'text'}
defaultValue={`${
env('VIEWER_INTERNAL_URL') ??
getViewerUrl({ isBuilder: true })
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
/>
<InputRightElement width="4.5rem">
<CopyButton
textToCopy={`${
env('VIEWER_INTERNAL_URL') ??
getViewerUrl({ isBuilder: true })
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
/>
</InputRightElement>

View File

@ -45,9 +45,7 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
})
const jsCode = parseInitContainerCode({
url: `${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl({ isBuilder: true })
}/${publicId}`,
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${publicId}`,
})
const headCode = prettier.format(
`${typebotJsHtml}<script>${jsCode}</script>`,

View File

@ -58,15 +58,13 @@ export const WordpressModal = ({
pr="4.5rem"
type={'text'}
defaultValue={`${
env('VIEWER_INTERNAL_URL') ??
getViewerUrl({ isBuilder: true })
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
/>
<InputRightElement width="4.5rem">
<CopyButton
textToCopy={`${
env('VIEWER_INTERNAL_URL') ??
getViewerUrl({ isBuilder: true })
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
/>
</InputRightElement>

View File

@ -16,7 +16,7 @@ import {
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { getChatsLimit, getStorageLimit } from 'utils'
import { getChatsLimit, getStorageLimit } from 'utils/pricing'
import { useStats } from '../hooks/useStats'
import { ResultsProvider } from '../ResultsProvider'
import { ResultsTableContainer } from './ResultsTableContainer'

View File

@ -23,10 +23,7 @@ export const SettingsPage = () => {
<SettingsSideMenu />
<Flex flex="1">
{publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
<TypebotViewer apiHost={getViewerUrl()} typebot={publicTypebot} />
)}
</Flex>
</Flex>

View File

@ -76,7 +76,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
</Heading>
{typebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
apiHost={getViewerUrl()}
typebot={parseTypebotToPublicTypebot(typebot)}
key={typebot.id}
style={{ borderRadius: '0.25rem' }}

View File

@ -18,10 +18,7 @@ export const ThemePage = () => {
<ThemeSideMenu />
<Flex flex="1">
{publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
<TypebotViewer apiHost={getViewerUrl()} typebot={publicTypebot} />
)}
</Flex>
</Flex>

View File

@ -8,7 +8,7 @@ import {
import { UnlockPlanAlertInfo } from '@/components/UnlockPlanAlertInfo'
import { WorkspaceInvitation, WorkspaceRole } from 'db'
import React from 'react'
import { getSeatsLimit } from 'utils'
import { getSeatsLimit } from 'utils/pricing'
import { AddMemberForm } from './AddMemberForm'
import { checkCanInviteMember } from './helpers'
import { MemberItem } from './MemberItem'

View File

@ -1,5 +1,5 @@
import { Plan } from 'db'
import { getSeatsLimit } from 'utils'
import { getSeatsLimit } from 'utils/pricing'
export function checkCanInviteMember({
plan,

View File

@ -3,8 +3,9 @@ import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth/api'
import { env, getSeatsLimit } from 'utils'
import { getSeatsLimit } from 'utils/pricing'
import { sendWorkspaceMemberInvitationEmail } from 'emails'
import { env } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)

View File

@ -1851,21 +1851,6 @@
"anyOf": [
{
"anyOf": [
{
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"sheetId": {
"type": "string"
},
"spreadsheetId": {
"type": "string"
}
},
"additionalProperties": false
},
{
"allOf": [
{
@ -1989,6 +1974,62 @@
"additionalProperties": false
}
]
},
{
"allOf": [
{
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"sheetId": {
"type": "string"
},
"spreadsheetId": {
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"Insert a row"
]
},
"cellsToInsert": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {
"type": "string"
},
"value": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"action",
"cellsToInsert"
],
"additionalProperties": false
}
]
}
]
},
@ -2015,10 +2056,10 @@
"action": {
"type": "string",
"enum": [
"Insert a row"
"Update a row"
]
},
"cellsToInsert": {
"cellsToUpsert": {
"type": "array",
"items": {
"type": "object",
@ -2038,11 +2079,29 @@
],
"additionalProperties": false
}
},
"referenceCell": {
"type": "object",
"properties": {
"column": {
"type": "string"
},
"value": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"required": [
"action",
"cellsToInsert"
"cellsToUpsert"
],
"additionalProperties": false
}
@ -2051,78 +2110,19 @@
]
},
{
"allOf": [
{
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"sheetId": {
"type": "string"
},
"spreadsheetId": {
"type": "string"
}
},
"additionalProperties": false
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"Update a row"
]
},
"cellsToUpsert": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {
"type": "string"
},
"value": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"referenceCell": {
"type": "object",
"properties": {
"column": {
"type": "string"
},
"value": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"required": [
"action",
"cellsToUpsert"
],
"additionalProperties": false
"sheetId": {
"type": "string"
},
"spreadsheetId": {
"type": "string"
}
]
},
"additionalProperties": false
}
]
}
@ -4850,6 +4850,36 @@
}
},
"additionalProperties": false
},
"logs": {
"type": "array",
"items": {
"allOf": [
{
"type": "object",
"properties": {
"status": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"status",
"description"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"details": {}
},
"additionalProperties": false
}
]
}
}
},
"required": [

View File

@ -20,7 +20,7 @@ import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
import { Plan } from 'db'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { chatsLimit, formatPrice, storageLimit } from 'utils'
import { chatsLimit, formatPrice, storageLimit } from 'utils/pricing'
type Props = {
starterPrice: string

View File

@ -10,7 +10,7 @@ import {
} from '@chakra-ui/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { formatPrice } from 'utils'
import { formatPrice } from 'utils/pricing'
import { CheckCircleIcon } from '../../../assets/icons/CheckCircleIcon'
import { Card, CardProps } from './Card'

View File

@ -14,12 +14,8 @@ import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
import { Plan } from 'db'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import {
chatsLimit,
computePrice,
parseNumberWithCommas,
storageLimit,
} from 'utils'
import { parseNumberWithCommas } from 'utils'
import { chatsLimit, computePrice, storageLimit } from 'utils/pricing'
import { PricingCard } from './PricingCard'
export const ProPlanCard = () => {

View File

@ -14,12 +14,8 @@ import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
import { Plan } from 'db'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import {
chatsLimit,
computePrice,
parseNumberWithCommas,
storageLimit,
} from 'utils'
import { parseNumberWithCommas } from 'utils'
import { chatsLimit, computePrice, storageLimit } from 'utils/pricing'
import { PricingCard } from './PricingCard'
export const StarterPlanCard = () => {

View File

@ -19,7 +19,7 @@ import { SocialMetaTags } from 'components/common/SocialMetaTags'
import { BackgroundPolygons } from 'components/Homepage/Hero/BackgroundPolygons'
import { PlanComparisonTables } from 'components/PricingPage/PlanComparisonTables'
import { useEffect, useState } from 'react'
import { formatPrice, prices } from 'utils'
import { formatPrice, prices } from 'utils/pricing'
import { StripeClimateLogo } from 'assets/logos/StripeClimateLogo'
import { FreePlanCard } from 'components/PricingPage/FreePlanCard'
import { StarterPlanCard } from 'components/PricingPage/StarterPlanCard'

View File

@ -15,6 +15,7 @@
"dependencies": {
"@sentry/nextjs": "7.31.1",
"@trpc/server": "10.9.0",
"@typebot.io/js": "workspace:*",
"@typebot.io/react": "workspace:*",
"aws-sdk": "2.1299.0",
"bot-engine": "workspace:*",

View File

@ -3,24 +3,21 @@ import { BackgroundType, Typebot } from 'models'
import { useRouter } from 'next/router'
import { SEO } from './Seo'
export type TypebotPageV2Props = {
export type TypebotPageProps = {
url: string
typebot: Pick<
Typebot,
'settings' | 'theme' | 'name' | 'isClosed' | 'isArchived' | 'publicId'
>
typebot?: Pick<Typebot, 'settings' | 'theme' | 'name' | 'publicId'>
}
export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
const { asPath, push } = useRouter()
export const TypebotPage = ({ url, typebot }: TypebotPageProps) => {
const { asPath, push, query } = useRouter()
const background = typebot.theme.general.background
const background = typebot?.theme.general.background
const clearQueryParamsIfNecessary = () => {
const hasQueryParams = asPath.includes('?')
if (
!hasQueryParams ||
!(typebot.settings.general.isHideQueryParamsEnabled ?? true)
!(typebot?.settings.general.isHideQueryParamsEnabled ?? true)
)
return
push(asPath.split('?')[0], undefined, { shallow: true })
@ -32,22 +29,22 @@ export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
height: '100vh',
// Set background color to avoid SSR flash
backgroundColor:
background.type === BackgroundType.COLOR
? background.content
background?.type === BackgroundType.COLOR
? background?.content
: 'white',
}}
>
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
/>
{typebot.publicId && (
<Standard
typebot={typebot.publicId}
onInit={clearQueryParamsIfNecessary}
{typebot && (
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
/>
)}
<Standard
typebot={typebot?.publicId ?? query.publicId?.toString() ?? 'n'}
onInit={clearQueryParamsIfNecessary}
/>
</div>
)
}

View File

@ -50,7 +50,7 @@ if (window.$chatwoot) {
}`
export const executeChatwootBlock = (
{ typebot: { variables } }: SessionState,
{ typebot: { variables }, isPreview }: SessionState,
block: ChatwootBlock
): ExecuteIntegrationResponse => {
const chatwootCode = parseChatwootOpenCode(block.options)
@ -71,5 +71,14 @@ export const executeChatwootBlock = (
},
},
},
logs: isPreview
? [
{
status: 'info',
description: 'Chatwoot block is not supported in preview',
details: null,
},
]
: undefined,
}
}

View File

@ -4,6 +4,7 @@ import {
VariableWithValue,
ComparisonOperators,
LogicalOperator,
ReplyLog,
} from 'models'
import { saveErrorLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc } from './helpers'
@ -20,7 +21,9 @@ export const getRow = async (
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise<ExecuteIntegrationResponse> => {
const { sheetId, cellsToExtract, referenceCell, filter } = options
if (!cellsToExtract || !sheetId || !referenceCell) return { outgoingEdgeId }
if (!sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const variables = state.typebot.variables
const resultId = state.result?.id
@ -40,11 +43,15 @@ export const getRow = async (
: matchFilter(row, filter)
)
if (filteredRows.length === 0) {
log = {
status: 'error',
description: `Couldn't find any rows matching the filter`,
}
await saveErrorLog({
resultId,
message: "Couldn't find reference cell",
message: log.description,
})
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}
const randomIndex = Math.floor(Math.random() * filteredRows.length)
const extractingColumns = cellsToExtract
@ -81,13 +88,18 @@ export const getRow = async (
newSessionState,
}
} catch (err) {
log = {
status: 'error',
description: `An error occurred while fetching the spreadsheet data`,
details: err,
}
await saveErrorLog({
resultId,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
})
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}
const matchFilter = (

View File

@ -1,4 +1,4 @@
import { SessionState, GoogleSheetsInsertRowOptions } from 'models'
import { SessionState, GoogleSheetsInsertRowOptions, ReplyLog } from 'models'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc, parseCellValues } from './helpers'
import { ExecuteIntegrationResponse } from '@/features/chat'
@ -10,8 +10,11 @@ export const insertRow = async (
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
): Promise<ExecuteIntegrationResponse> => {
console.log('insertRow', options)
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
@ -23,18 +26,27 @@ export const insertRow = async (
await doc.loadInfo()
const sheet = doc.sheetsById[options.sheetId]
await sheet.addRow(parsedValues)
log = {
status: 'success',
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: 'Succesfully inserted row',
message: log?.description,
}))
} catch (err) {
log = {
status: 'error',
description: `An error occured while inserting the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
}))
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}

View File

@ -1,4 +1,4 @@
import { SessionState, GoogleSheetsUpdateRowOptions } from 'models'
import { SessionState, GoogleSheetsUpdateRowOptions, ReplyLog } from 'models'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc, parseCellValues } from './helpers'
import { TRPCError } from '@trpc/server'
@ -16,6 +16,8 @@ export const updateRow = async (
if (!options.cellsToUpsert || !sheetId || !referenceCell)
return { outgoingEdgeId }
let log: ReplyLog | undefined
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
@ -45,18 +47,27 @@ export const updateRow = async (
rows[updatingRowIndex][key] = parsedValues[key]
}
await rows[updatingRowIndex].save()
log = log = {
status: 'success',
description: `Succesfully updated row in ${doc.title} > ${sheet.title}`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: 'Succesfully updated row',
message: log.description,
}))
} catch (err) {
log = {
status: 'error',
description: `An error occured while updating the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
}))
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}

View File

@ -19,11 +19,21 @@ import { decrypt } from 'utils/api'
import { defaultFrom, defaultTransportOptions } from '../constants'
export const executeSendEmailBlock = async (
{ result, typebot }: SessionState,
{ result, typebot, isPreview }: SessionState,
block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => {
const { options } = block
const { variables } = typebot
if (isPreview)
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [
{
status: 'info',
description: 'Emails are not sent in preview mode',
},
],
}
await sendEmail({
typebotId: typebot.id,
resultId: result?.id,

View File

@ -19,6 +19,7 @@ import {
ResultValues,
PublicTypebot,
KeyValue,
ReplyLog,
} from 'models'
import { stringify } from 'qs'
import { byId, omit, parseAnswers } from 'utils'
@ -31,6 +32,7 @@ export const executeWebhookBlock = async (
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
let log: ReplyLog | undefined
const webhook = (await prisma.webhook.findUnique({
where: { id: block.webhookId },
})) as Webhook | null
@ -56,20 +58,27 @@ export const executeWebhookBlock = async (
const isError = status.startsWith('4') || status.startsWith('5')
if (isError) {
log = {
status: 'error',
description: `Webhook returned error: ${webhookResponse.data}`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
}
result &&
(await saveErrorLog({
resultId: result.id,
message: `Webhook returned error: ${webhookResponse.data}`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(
0,
1000
),
message: log.description,
details: log.details,
}))
} else {
log = {
status: 'success',
description: `Webhook executed successfully!`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: `Webhook returned success: ${webhookResponse.data}`,
message: log.description,
details: JSON.stringify(webhookResponse.data, null, 2).substring(
0,
1000
@ -102,7 +111,7 @@ export const executeWebhookBlock = async (
}
}
return { outgoingEdgeId: block.outgoingEdgeId }
return { outgoingEdgeId: block.outgoingEdgeId, logs: log ? [log] : undefined }
}
const prepareWebhookAttributes = (

View File

@ -40,8 +40,15 @@ export const sendMessageProcedure = publicProcedure
const session = sessionId ? await getSession(sessionId) : null
if (!session) {
const { sessionId, typebot, messages, input, resultId, dynamicTheme } =
await startSession(startParams)
const {
sessionId,
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
} = await startSession(startParams)
return {
sessionId,
typebot: typebot
@ -55,9 +62,10 @@ export const sendMessageProcedure = publicProcedure
input,
resultId,
dynamicTheme,
logs,
}
} else {
const { messages, input, logic, newSessionState, integrations } =
const { messages, input, logic, newSessionState, integrations, logs } =
await continueBotFlow(session.state)(message)
await prisma.chatSession.updateMany({
@ -73,6 +81,7 @@ export const sendMessageProcedure = publicProcedure
logic,
integrations,
dynamicTheme: parseDynamicThemeReply(newSessionState),
logs,
}
}
})
@ -84,6 +93,9 @@ const startSession = async (startParams?: StartParams) => {
message: 'No typebot provided in startParams',
})
const isPreview =
startParams?.isPreview || typeof startParams?.typebot !== 'string'
const typebot = await getTypebot(startParams)
const startVariables = startParams.prefilledVariables
@ -92,6 +104,7 @@ const startSession = async (startParams?: StartParams) => {
const result = await getResult({
...startParams,
isPreview,
typebot: typebot.id,
startVariables,
isNewResultOnRefreshEnabled:
@ -112,7 +125,7 @@ const startSession = async (startParams?: StartParams) => {
result: result
? { id: result.id, variables: result.variables, hasStarted: false }
: undefined,
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
isPreview,
currentTypebotId: typebot.id,
dynamicTheme: parseDynamicThemeInState(typebot.theme),
}
@ -122,6 +135,7 @@ const startSession = async (startParams?: StartParams) => {
input,
logic,
newSessionState: newInitialState,
logs,
} = await startBotFlow(initialState, startParams.startGroupId)
if (!input)
@ -138,6 +152,7 @@ const startSession = async (startParams?: StartParams) => {
),
},
dynamicTheme: parseDynamicThemeReply(newInitialState),
logs,
}
const sessionState: ChatSession['state'] = {
@ -170,6 +185,7 @@ const startSession = async (startParams?: StartParams) => {
input,
logic,
dynamicTheme: parseDynamicThemeReply(newInitialState),
logs,
} satisfies ChatReply
}

View File

@ -27,6 +27,7 @@ export const executeGroup =
const messages: ChatReply['messages'] = currentReply?.messages ?? []
let logic: ChatReply['logic'] = currentReply?.logic
let integrations: ChatReply['integrations'] = currentReply?.integrations
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let newSessionState = state
@ -58,6 +59,9 @@ export const executeGroup =
blockId: block.id,
},
},
logic,
integrations,
logs,
}
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block)
@ -67,9 +71,11 @@ export const executeGroup =
if (!executionResponse) continue
if ('logic' in executionResponse && executionResponse.logic)
logic = executionResponse.logic
logic = { ...logic, ...executionResponse.logic }
if ('integrations' in executionResponse && executionResponse.integrations)
integrations = executionResponse.integrations
integrations = { ...integrations, ...executionResponse.integrations }
if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
newSessionState = executionResponse.newSessionState
if (executionResponse.outgoingEdgeId) {
@ -78,19 +84,23 @@ export const executeGroup =
}
}
if (!nextEdgeId) return { messages, newSessionState, logic, integrations }
if (!nextEdgeId)
return { messages, newSessionState, logic, integrations, logs }
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext
if (!nextGroup) {
return { messages, newSessionState, logic, integrations }
return { messages, newSessionState, logic, integrations, logs }
}
return executeGroup(newSessionState, { messages, logic, integrations })(
nextGroup.group
)
return executeGroup(newSessionState, {
messages,
logic,
integrations,
logs,
})(nextGroup.group)
}
const computeRuntimeOptions =

View File

@ -5,9 +5,9 @@ export type EdgeId = string
export type ExecuteLogicResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
} & Pick<ChatReply, 'logic'>
} & Pick<ChatReply, 'logic' | 'logs'>
export type ExecuteIntegrationResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
} & Pick<ChatReply, 'integrations'>
} & Pick<ChatReply, 'integrations' | 'logs'>

View File

@ -5,7 +5,8 @@ import {
sendReachedChatsLimitEmail,
} from 'emails'
import { Workspace } from 'models'
import { env, getChatsLimit, isDefined } from 'utils'
import { env, isDefined } from 'utils'
import { getChatsLimit } from 'utils/pricing'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8

View File

@ -11,7 +11,7 @@ import Stripe from 'stripe'
import Cors from 'cors'
import { PaymentInputOptions, StripeCredentialsData, Variable } from 'models'
import prisma from '@/lib/prisma'
import { parseVariables } from 'bot-engine'
import { parseVariables } from '@/features/variables'
const cors = initMiddleware(Cors())

View File

@ -11,7 +11,6 @@ import {
WebhookBlock,
HttpMethod,
} from 'models'
import { parseVariables } from 'bot-engine'
import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got'
import { byId, omit, parseAnswers } from 'utils'
@ -25,6 +24,7 @@ import {
getLinkedTypebots,
getLinkedTypebotsChildren,
} from '@/features/blocks/logic/typebotLink/api'
import { parseVariables } from '@/features/variables'
const cors = initMiddleware(Cors())

View File

@ -2,7 +2,8 @@ import prisma from '@/lib/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
import { byId, env, getStorageLimit, isDefined } from 'utils'
import { byId, env, isDefined } from 'utils'
import { getStorageLimit } from 'utils/pricing'
import {
sendAlmostReachedStorageLimitEmail,
sendReachedStorageLimitEmail,

View File

@ -5,15 +5,7 @@ import cors from 'nextjs-cors'
import { NextApiRequest, NextApiResponse } from 'next'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res, {
origin: [
'https://docs.typebot.io',
'https://app.typebot.io',
'http://localhost:3005',
'http://localhost:3006',
'http://localhost:3000',
],
})
await cors(req, res)
return createOpenApiNextHandler({
router: appRouter,
@ -25,4 +17,5 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
},
})(req, res)
}
export default handler

View File

@ -1,10 +1,8 @@
import { IncomingMessage } from 'http'
import { NotFoundPage } from '@/components/NotFoundPage'
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { env, getViewerUrl, isNotDefined } from 'utils'
import prisma from '@/lib/prisma'
import { TypebotPageV2, TypebotPageV2Props } from '@/components/TypebotPageV2'
import { ErrorPage } from '@/components/ErrorPage'
import { TypebotPage, TypebotPageProps } from '@/components/TypebotPageV2'
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
@ -51,18 +49,16 @@ export const getServerSideProps: GetServerSideProps = async (
const getTypebotFromPublicId = async (
publicId?: string
): Promise<TypebotPageV2Props['typebot'] | null> => {
): Promise<TypebotPageProps['typebot'] | null> => {
const typebot = (await prisma.typebot.findUnique({
where: { publicId: publicId ?? '' },
select: {
theme: true,
name: true,
settings: true,
isArchived: true,
isClosed: true,
publicId: true,
},
})) as TypebotPageV2Props['typebot'] | null
})) as TypebotPageProps['typebot'] | null
if (isNotDefined(typebot)) return null
return typebot
}
@ -74,11 +70,8 @@ const getHost = (
forwardedHost: req?.headers['x-forwarded-host'] as string | undefined,
})
const App = ({ typebot, url }: TypebotPageV2Props) => {
if (!typebot || typebot.isArchived) return <NotFoundPage />
if (typebot.isClosed)
return <ErrorPage error={new Error('This bot is now closed')} />
return <TypebotPageV2 typebot={typebot} url={url} />
}
const App = ({ typebot, url }: TypebotPageProps) => (
<TypebotPage typebot={typebot} url={url} />
)
export default App

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'

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