feat(theme): ✨ Custom avatars
This commit is contained in:
@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import { HostAvatar } from '../avatars/HostAvatar'
|
||||
import { Avatar } from '../avatars/Avatar'
|
||||
import { useFrame } from 'react-frame-component'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
|
||||
|
||||
export const AvatarSideContainer = () => {
|
||||
export const AvatarSideContainer = ({
|
||||
hostAvatarSrc,
|
||||
}: {
|
||||
hostAvatarSrc: string
|
||||
}) => {
|
||||
const { lastBubblesTopOffset } = useHostAvatars()
|
||||
const { typebot } = useTypebot()
|
||||
const { window, document } = useFrame()
|
||||
const [marginBottom, setMarginBottom] = useState(
|
||||
window.innerWidth < 400 ? 38 : 48
|
||||
@ -44,7 +46,7 @@ export const AvatarSideContainer = () => {
|
||||
transition: 'top 350ms ease-out',
|
||||
}}
|
||||
>
|
||||
<HostAvatar typebotName={typebot.name} />
|
||||
<Avatar avatarSrc={hostAvatarSrc} />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
))}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { executeLogic } from 'services/logic'
|
||||
import { executeIntegration } from 'services/integration'
|
||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||
import { parseVariables } from 'index'
|
||||
|
||||
type ChatBlockProps = {
|
||||
steps: PublicStep[]
|
||||
@ -108,7 +109,13 @@ export const ChatBlock = ({
|
||||
return (
|
||||
<div className="flex">
|
||||
<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">
|
||||
<TransitionGroup>
|
||||
{displayedSteps
|
||||
|
@ -9,6 +9,8 @@ import { DateForm } from './inputs/DateForm'
|
||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||
import { HostBubble } from './bubbles/HostBubble'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { parseVariables } from 'index'
|
||||
|
||||
export const ChatStep = ({
|
||||
step,
|
||||
@ -38,6 +40,7 @@ const InputChatStep = ({
|
||||
step: InputStep
|
||||
onSubmit: (value: string, isRetry: boolean) => void
|
||||
}) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { addNewAvatarOffset } = useHostAvatars()
|
||||
const [answer, setAnswer] = useState<string>()
|
||||
|
||||
@ -52,7 +55,15 @@ const InputChatStep = ({
|
||||
}
|
||||
|
||||
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) {
|
||||
case InputStepType.TEXT:
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { Avatar } from 'components/avatars/Avatar'
|
||||
import React from 'react'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
|
||||
interface Props {
|
||||
message: string
|
||||
showAvatar: boolean
|
||||
avatarSrc: string
|
||||
}
|
||||
|
||||
export const GuestBubble = ({ message }: Props): JSX.Element => {
|
||||
export const GuestBubble = ({
|
||||
message,
|
||||
showAvatar,
|
||||
avatarSrc,
|
||||
}: Props): JSX.Element => {
|
||||
return (
|
||||
<CSSTransition classNames="bubble" timeout={1000}>
|
||||
<div className="flex justify-end mb-2 items-center">
|
||||
@ -16,6 +23,7 @@ export const GuestBubble = ({ message }: Props): JSX.Element => {
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{showAvatar && <Avatar avatarSrc={avatarSrc} />}
|
||||
</div>
|
||||
</div>
|
||||
</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,60 +1,46 @@
|
||||
import React from 'react'
|
||||
|
||||
type DefaultAvatarProps = {
|
||||
displayName?: string
|
||||
size?: 'extra-small' | 'small' | 'medium' | 'large' | 'full'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const DefaultAvatar = ({
|
||||
displayName,
|
||||
}: DefaultAvatarProps): JSX.Element => {
|
||||
export const DefaultAvatar = (): JSX.Element => {
|
||||
return (
|
||||
<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'
|
||||
}
|
||||
data-testid="default-avatar"
|
||||
>
|
||||
<Background
|
||||
<svg
|
||||
width="75"
|
||||
height="75"
|
||||
viewBox="0 0 75 75"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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>
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
const Background = ({ className }: { className: string }) => (
|
||||
<svg
|
||||
width="75"
|
||||
height="75"
|
||||
viewBox="0 0 75 75"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
)
|
||||
|
@ -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 React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot: PublicTypebot
|
||||
@ -24,6 +30,11 @@ export const TypebotContext = ({
|
||||
}) => {
|
||||
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) => {
|
||||
setLocalTypebot((typebot) => ({
|
||||
...typebot,
|
||||
|
Reference in New Issue
Block a user