feat(theme): ✨ Custom avatars
This commit is contained in:
38
apps/builder/assets/DefaultAvatar.tsx
Normal file
38
apps/builder/assets/DefaultAvatar.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Icon, IconProps } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const DefaultAvatar = (props: IconProps) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
viewBox="0 0 75 75"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
boxSize="40px"
|
||||||
|
data-testid="default-avatar"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask id="mask0" x="0" y="0" mask-type="alpha">
|
||||||
|
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0)">
|
||||||
|
<rect x="-30" y="-43" width="131" height="154" fill="#0042DA" />
|
||||||
|
<rect
|
||||||
|
x="2.50413"
|
||||||
|
y="120.333"
|
||||||
|
width="81.5597"
|
||||||
|
height="86.4577"
|
||||||
|
rx="2.5"
|
||||||
|
transform="rotate(-52.6423 2.50413 120.333)"
|
||||||
|
stroke="#FED23D"
|
||||||
|
strokeWidth="5"
|
||||||
|
/>
|
||||||
|
<circle cx="76.5" cy="-1.5" r="29" stroke="#FF8E20" strokeWidth="5" />
|
||||||
|
<path
|
||||||
|
d="M-49.8224 22L-15.5 -40.7879L18.8224 22H-49.8224Z"
|
||||||
|
stroke="#F7F8FF"
|
||||||
|
strokeWidth="5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Button, Flex, HStack, Input, Stack, Text } from '@chakra-ui/react'
|
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
|
||||||
import { SearchContextManager } from '@giphy/react-components'
|
import { SearchContextManager } from '@giphy/react-components'
|
||||||
import { UploadButton } from '../buttons/UploadButton'
|
import { UploadButton } from '../buttons/UploadButton'
|
||||||
import { GiphySearch } from './GiphySearch'
|
import { GiphySearch } from './GiphySearch'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
|
import { InputWithVariableButton } from '../TextboxWithVariableButton'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
@@ -102,16 +103,12 @@ const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [debouncedImageUrl])
|
}, [debouncedImageUrl])
|
||||||
|
|
||||||
const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setImageUrl(e.target.value)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack py="2">
|
<Stack py="2">
|
||||||
<Input
|
<InputWithVariableButton
|
||||||
my="2"
|
|
||||||
placeholder={'Paste the image link...'}
|
placeholder={'Paste the image link...'}
|
||||||
onChange={handleImageUrlChange}
|
onChange={setImageUrl}
|
||||||
value={imageUrl}
|
initialValue={imageUrl}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
82
apps/builder/components/theme/ChatSettings/AvatarForm.tsx
Normal file
82
apps/builder/components/theme/ChatSettings/AvatarForm.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { AvatarProps } from 'models'
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Image,
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Portal,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||||
|
import { DefaultAvatar } from 'assets/DefaultAvatar'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
avatarProps?: AvatarProps
|
||||||
|
isDefaultCheck?: boolean
|
||||||
|
onAvatarChange: (avatarProps: AvatarProps) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarForm = ({
|
||||||
|
title,
|
||||||
|
avatarProps,
|
||||||
|
isDefaultCheck = false,
|
||||||
|
onAvatarChange,
|
||||||
|
}: Props) => {
|
||||||
|
const isChecked = avatarProps ? avatarProps.isEnabled : isDefaultCheck
|
||||||
|
const handleOnCheck = () =>
|
||||||
|
onAvatarChange({ ...avatarProps, isEnabled: !isChecked })
|
||||||
|
const handleImageUrl = (url: string) =>
|
||||||
|
onAvatarChange({ isEnabled: isChecked, url })
|
||||||
|
return (
|
||||||
|
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||||
|
<Flex justifyContent="space-between">
|
||||||
|
<HStack>
|
||||||
|
<Heading as="label" fontSize="lg" htmlFor={title} mb="1">
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
<Switch isChecked={isChecked} id={title} onChange={handleOnCheck} />
|
||||||
|
</HStack>
|
||||||
|
{isChecked && (
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{avatarProps?.url ? (
|
||||||
|
<Image
|
||||||
|
src={avatarProps.url}
|
||||||
|
alt="Website image"
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ filter: 'brightness(.9)' }}
|
||||||
|
transition="filter 200ms"
|
||||||
|
rounded="full"
|
||||||
|
boxSize="40px"
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<DefaultAvatar
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ filter: 'brightness(.9)' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<Portal>
|
||||||
|
<PopoverContent p="4">
|
||||||
|
<ImageUploadContent
|
||||||
|
url={avatarProps?.url}
|
||||||
|
onSubmit={handleImageUrl}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Heading, Stack } from '@chakra-ui/react'
|
import { Heading, Stack } from '@chakra-ui/react'
|
||||||
import { ChatTheme, ContainerColors, InputColors } from 'models'
|
import { AvatarProps, ChatTheme, ContainerColors, InputColors } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { AvatarForm } from './AvatarForm'
|
||||||
import { ButtonsTheme } from './ButtonsTheme'
|
import { ButtonsTheme } from './ButtonsTheme'
|
||||||
import { GuestBubbles } from './GuestBubbles'
|
import { GuestBubbles } from './GuestBubbles'
|
||||||
import { HostBubbles } from './HostBubbles'
|
import { HostBubbles } from './HostBubbles'
|
||||||
@@ -21,8 +22,24 @@ export const ChatThemeSettings = ({ chatTheme, onChatThemeChange }: Props) => {
|
|||||||
const handleInputsChange = (inputs: InputColors) =>
|
const handleInputsChange = (inputs: InputColors) =>
|
||||||
onChatThemeChange({ ...chatTheme, inputs })
|
onChatThemeChange({ ...chatTheme, inputs })
|
||||||
|
|
||||||
|
const handleHostAvatarChange = (hostAvatar: AvatarProps) =>
|
||||||
|
onChatThemeChange({ ...chatTheme, hostAvatar })
|
||||||
|
const handleGuestAvatarChange = (guestAvatar: AvatarProps) =>
|
||||||
|
onChatThemeChange({ ...chatTheme, guestAvatar })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
|
<AvatarForm
|
||||||
|
title="Bot avatar"
|
||||||
|
avatarProps={chatTheme.hostAvatar}
|
||||||
|
isDefaultCheck
|
||||||
|
onAvatarChange={handleHostAvatarChange}
|
||||||
|
/>
|
||||||
|
<AvatarForm
|
||||||
|
title="User avatar"
|
||||||
|
avatarProps={chatTheme.guestAvatar}
|
||||||
|
onAvatarChange={handleGuestAvatarChange}
|
||||||
|
/>
|
||||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||||
<Heading fontSize="lg">Bot bubbles</Heading>
|
<Heading fontSize="lg">Bot bubbles</Heading>
|
||||||
<HostBubbles
|
<HostBubbles
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { generate } from 'short-uuid'
|
|||||||
import { importTypebotInDatabase } from '../services/database'
|
import { importTypebotInDatabase } from '../services/database'
|
||||||
import { typebotViewer } from '../services/selectorUtils'
|
import { typebotViewer } from '../services/selectorUtils'
|
||||||
|
|
||||||
|
const hostAvatarUrl =
|
||||||
|
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80'
|
||||||
|
const guestAvatarUrl =
|
||||||
|
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
||||||
|
|
||||||
test.describe.parallel('Theme page', () => {
|
test.describe.parallel('Theme page', () => {
|
||||||
test.describe('General', () => {
|
test.describe('General', () => {
|
||||||
test('should reflect change in real-time', async ({ page }) => {
|
test('should reflect change in real-time', async ({ page }) => {
|
||||||
@@ -40,7 +45,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Chat', () => {
|
test.describe('Chat', () => {
|
||||||
test('should reflect change in real-time', async ({ page }) => {
|
test.only('should reflect change in real-time', async ({ page }) => {
|
||||||
const typebotId = 'chat-theme-typebot'
|
const typebotId = 'chat-theme-typebot'
|
||||||
try {
|
try {
|
||||||
await importTypebotInDatabase(
|
await importTypebotInDatabase(
|
||||||
@@ -53,7 +58,23 @@ test.describe.parallel('Theme page', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await page.click('button:has-text("Chat")')
|
await page.click('button:has-text("Chat")')
|
||||||
await page.waitForTimeout(300)
|
|
||||||
|
// Host avatar
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator('[data-testid="default-avatar"]')
|
||||||
|
).toBeVisible()
|
||||||
|
await page.click('[data-testid="default-avatar"]')
|
||||||
|
await page.click('button:has-text("Embed link")')
|
||||||
|
await page.fill(
|
||||||
|
'input[placeholder="Paste the image link..."]',
|
||||||
|
hostAvatarUrl
|
||||||
|
)
|
||||||
|
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
hostAvatarUrl
|
||||||
|
)
|
||||||
|
await page.click('text=Bot avatar')
|
||||||
|
await expect(typebotViewer(page).locator('img')).toBeHidden()
|
||||||
|
|
||||||
// Host bubbles
|
// Host bubbles
|
||||||
await page.click(
|
await page.click(
|
||||||
@@ -105,6 +126,22 @@ test.describe.parallel('Theme page', () => {
|
|||||||
)
|
)
|
||||||
await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)')
|
await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)')
|
||||||
|
|
||||||
|
// Guest avatar
|
||||||
|
await page.click('text=User avatar')
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator('[data-testid="default-avatar"]')
|
||||||
|
).toBeVisible()
|
||||||
|
await page.click('[data-testid="default-avatar"]')
|
||||||
|
await page.click('button:has-text("Embed link")')
|
||||||
|
await page.fill(
|
||||||
|
'input[placeholder="Paste the image link..."]',
|
||||||
|
guestAvatarUrl
|
||||||
|
)
|
||||||
|
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
guestAvatarUrl
|
||||||
|
)
|
||||||
|
|
||||||
// Input
|
// Input
|
||||||
await page.click(
|
await page.click(
|
||||||
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
|
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTypebot } from '../../contexts/TypebotContext'
|
import { Avatar } from '../avatars/Avatar'
|
||||||
import { HostAvatar } from '../avatars/HostAvatar'
|
|
||||||
import { useFrame } from 'react-frame-component'
|
import { useFrame } from 'react-frame-component'
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||||
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
|
||||||
|
|
||||||
export const AvatarSideContainer = () => {
|
export const AvatarSideContainer = ({
|
||||||
|
hostAvatarSrc,
|
||||||
|
}: {
|
||||||
|
hostAvatarSrc: string
|
||||||
|
}) => {
|
||||||
const { lastBubblesTopOffset } = useHostAvatars()
|
const { lastBubblesTopOffset } = useHostAvatars()
|
||||||
const { typebot } = useTypebot()
|
|
||||||
const { window, document } = useFrame()
|
const { window, document } = useFrame()
|
||||||
const [marginBottom, setMarginBottom] = useState(
|
const [marginBottom, setMarginBottom] = useState(
|
||||||
window.innerWidth < 400 ? 38 : 48
|
window.innerWidth < 400 ? 38 : 48
|
||||||
@@ -44,7 +46,7 @@ export const AvatarSideContainer = () => {
|
|||||||
transition: 'top 350ms ease-out',
|
transition: 'top 350ms ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HostAvatar typebotName={typebot.name} />
|
<Avatar avatarSrc={hostAvatarSrc} />
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { executeLogic } from 'services/logic'
|
import { executeLogic } from 'services/logic'
|
||||||
import { executeIntegration } from 'services/integration'
|
import { executeIntegration } from 'services/integration'
|
||||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||||
|
import { parseVariables } from 'index'
|
||||||
|
|
||||||
type ChatBlockProps = {
|
type ChatBlockProps = {
|
||||||
steps: PublicStep[]
|
steps: PublicStep[]
|
||||||
@@ -108,7 +109,13 @@ export const ChatBlock = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<HostAvatarsContext>
|
<HostAvatarsContext>
|
||||||
<AvatarSideContainer />
|
{(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
||||||
|
<AvatarSideContainer
|
||||||
|
hostAvatarSrc={parseVariables(typebot.variables)(
|
||||||
|
typebot.theme.chat.hostAvatar?.url
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<TransitionGroup>
|
<TransitionGroup>
|
||||||
{displayedSteps
|
{displayedSteps
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { DateForm } from './inputs/DateForm'
|
|||||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||||
import { HostBubble } from './bubbles/HostBubble'
|
import { HostBubble } from './bubbles/HostBubble'
|
||||||
import { isInputValid } from 'services/inputs'
|
import { isInputValid } from 'services/inputs'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { parseVariables } from 'index'
|
||||||
|
|
||||||
export const ChatStep = ({
|
export const ChatStep = ({
|
||||||
step,
|
step,
|
||||||
@@ -38,6 +40,7 @@ const InputChatStep = ({
|
|||||||
step: InputStep
|
step: InputStep
|
||||||
onSubmit: (value: string, isRetry: boolean) => void
|
onSubmit: (value: string, isRetry: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
const { addNewAvatarOffset } = useHostAvatars()
|
const { addNewAvatarOffset } = useHostAvatars()
|
||||||
const [answer, setAnswer] = useState<string>()
|
const [answer, setAnswer] = useState<string>()
|
||||||
|
|
||||||
@@ -52,7 +55,15 @@ const InputChatStep = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
return <GuestBubble message={answer} />
|
return (
|
||||||
|
<GuestBubble
|
||||||
|
message={answer}
|
||||||
|
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||||
|
avatarSrc={parseVariables(typebot.variables)(
|
||||||
|
typebot.theme.chat.guestAvatar?.url
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case InputStepType.TEXT:
|
case InputStepType.TEXT:
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { Avatar } from 'components/avatars/Avatar'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CSSTransition } from 'react-transition-group'
|
import { CSSTransition } from 'react-transition-group'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: string
|
message: string
|
||||||
|
showAvatar: boolean
|
||||||
|
avatarSrc: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GuestBubble = ({ message }: Props): JSX.Element => {
|
export const GuestBubble = ({
|
||||||
|
message,
|
||||||
|
showAvatar,
|
||||||
|
avatarSrc,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<CSSTransition classNames="bubble" timeout={1000}>
|
<CSSTransition classNames="bubble" timeout={1000}>
|
||||||
<div className="flex justify-end mb-2 items-center">
|
<div className="flex justify-end mb-2 items-center">
|
||||||
@@ -16,6 +23,7 @@ export const GuestBubble = ({ message }: Props): JSX.Element => {
|
|||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
|
{showAvatar && <Avatar avatarSrc={avatarSrc} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
|
|||||||
24
packages/bot-engine/src/components/avatars/Avatar.tsx
Normal file
24
packages/bot-engine/src/components/avatars/Avatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { DefaultAvatar } from './DefaultAvatar'
|
||||||
|
|
||||||
|
export const Avatar = ({ avatarSrc }: { avatarSrc: string }): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full rounded-full text-2xl md:text-4xl text-center xs:w-10 xs:h-10">
|
||||||
|
{avatarSrc !== '' ? (
|
||||||
|
<figure
|
||||||
|
className={
|
||||||
|
'flex justify-center items-center rounded-full text-white w-6 h-6 text-sm relative xs:w-full xs:h-full xs:text-xl'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={avatarSrc}
|
||||||
|
alt="Bot avatar"
|
||||||
|
className="rounded-full object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
) : (
|
||||||
|
<DefaultAvatar />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,38 +1,22 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type DefaultAvatarProps = {
|
export const DefaultAvatar = (): JSX.Element => {
|
||||||
displayName?: string
|
|
||||||
size?: 'extra-small' | 'small' | 'medium' | 'large' | 'full'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultAvatar = ({
|
|
||||||
displayName,
|
|
||||||
}: DefaultAvatarProps): JSX.Element => {
|
|
||||||
return (
|
return (
|
||||||
<figure
|
<figure
|
||||||
className={
|
className={
|
||||||
'flex justify-center items-center rounded-full text-white w-6 h-6 text-sm relative xs:w-full xs:h-full xs:text-xl'
|
'flex justify-center items-center rounded-full text-white w-6 h-6 text-sm relative xs:w-full xs:h-full xs:text-xl'
|
||||||
}
|
}
|
||||||
|
data-testid="default-avatar"
|
||||||
>
|
>
|
||||||
<Background
|
|
||||||
className={
|
|
||||||
'absolute top-0 left-0 w-6 h-6 xs:w-full xs:h-full xs:text-xl'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p style={{ zIndex: 0 }}>{displayName && displayName[0].toUpperCase()}</p>
|
|
||||||
</figure>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Background = ({ className }: { className: string }) => (
|
|
||||||
<svg
|
<svg
|
||||||
width="75"
|
width="75"
|
||||||
height="75"
|
height="75"
|
||||||
viewBox="0 0 75 75"
|
viewBox="0 0 75 75"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className={className}
|
className={
|
||||||
|
'absolute top-0 left-0 w-6 h-6 xs:w-full xs:h-full xs:text-xl'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<mask id="mask0" x="0" y="0" mask-type="alpha">
|
<mask id="mask0" x="0" y="0" mask-type="alpha">
|
||||||
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
|
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
|
||||||
@@ -57,4 +41,6 @@ const Background = ({ className }: { className: string }) => (
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
</figure>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { DefaultAvatar } from './DefaultAvatar'
|
|
||||||
|
|
||||||
export const HostAvatar = ({
|
|
||||||
typebotName,
|
|
||||||
}: {
|
|
||||||
typebotName: string
|
|
||||||
}): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full rounded-full text-2xl md:text-4xl text-center xs:w-10 xs:h-10">
|
|
||||||
<DefaultAvatar displayName={typebotName} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Edge, PublicTypebot } from 'models'
|
import { Edge, PublicTypebot } from 'models'
|
||||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
import React, {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
const typebotContext = createContext<{
|
const typebotContext = createContext<{
|
||||||
typebot: PublicTypebot
|
typebot: PublicTypebot
|
||||||
@@ -24,6 +30,11 @@ export const TypebotContext = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalTypebot({ ...localTypebot, theme: typebot.theme })
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [typebot.theme])
|
||||||
|
|
||||||
const updateVariableValue = (variableId: string, value: string) => {
|
const updateVariableValue = (variableId: string, value: string) => {
|
||||||
setLocalTypebot((typebot) => ({
|
setLocalTypebot((typebot) => ({
|
||||||
...typebot,
|
...typebot,
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ export type GeneralTheme = {
|
|||||||
background: Background
|
background: Background
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AvatarProps = {
|
||||||
|
isEnabled: boolean
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ChatTheme = {
|
export type ChatTheme = {
|
||||||
|
hostAvatar?: AvatarProps
|
||||||
|
guestAvatar?: AvatarProps
|
||||||
hostBubbles: ContainerColors
|
hostBubbles: ContainerColors
|
||||||
guestBubbles: ContainerColors
|
guestBubbles: ContainerColors
|
||||||
buttons: ContainerColors
|
buttons: ContainerColors
|
||||||
|
|||||||
Reference in New Issue
Block a user