⚡ (engine) Improve engine overall robustness
This commit is contained in:
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -106,7 +106,7 @@ export const PreviewDrawer = () => {
|
||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||
>
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
apiHost={getViewerUrl()}
|
||||
typebot={publicTypebot}
|
||||
onNewGroupVisible={setPreviewingEdge}
|
||||
onNewLog={handleNewLog}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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 />
|
||||
|
@ -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,
|
||||
}),
|
||||
{
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>`,
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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' }}
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Plan } from 'db'
|
||||
import { getSeatsLimit } from 'utils'
|
||||
import { getSeatsLimit } from 'utils/pricing'
|
||||
|
||||
export function checkCanInviteMember({
|
||||
plan,
|
||||
|
@ -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)
|
||||
|
@ -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": [
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 = () => {
|
||||
|
@ -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'
|
||||
|
@ -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:*",
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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 = (
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 = (
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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'>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 |
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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 = {
|
||||
|
3
packages/js/src/components/index.ts
Normal file
3
packages/js/src/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './SendButton'
|
||||
export * from './TypingBubble'
|
||||
export * from './inputs'
|
@ -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,
|
@ -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" />
|
||||
)
|
||||
}
|
@ -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
9
packages/js/src/env.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
export {}
|
||||
|
||||
declare module 'solid-js' {
|
||||
namespace JSX {
|
||||
interface CustomEvents {
|
||||
click: MouseEvent
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TypingEmulation } from 'models'
|
||||
import type { TypingEmulation } from 'models'
|
||||
|
||||
export const computeTypingDuration = (
|
||||
bubbleContent: string,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { sendGaEvent } from '@/lib/gtag'
|
||||
import { GoogleAnalyticsOptions } from 'models'
|
||||
import type { GoogleAnalyticsOptions } from 'models'
|
||||
|
||||
export const executeGoogleAnalyticsBlock = async (
|
||||
options: GoogleAnalyticsOptions
|
||||
|
@ -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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RedirectOptions } from 'models'
|
||||
import type { RedirectOptions } from 'models'
|
||||
|
||||
export const executeRedirect = ({ url, isNewTab }: RedirectOptions) => {
|
||||
if (!url) return
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -2,6 +2,5 @@ export type PopupParams = {
|
||||
autoShowDelay?: number
|
||||
theme?: {
|
||||
width?: string
|
||||
backgroundColor?: string
|
||||
}
|
||||
}
|
||||
|
47
packages/js/src/features/standard/components/Standard.tsx
Normal file
47
packages/js/src/features/standard/components/Standard.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
1
packages/js/src/features/standard/components/index.ts
Normal file
1
packages/js/src/features/standard/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Standard'
|
1
packages/js/src/features/standard/index.ts
Normal file
1
packages/js/src/features/standard/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './components'
|
@ -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
Reference in New Issue
Block a user