2
0

🚀 Init preview and typebot cotext in editor

This commit is contained in:
Baptiste Arnaud
2021-12-22 14:59:07 +01:00
parent a54e42f255
commit b7cdc0d14a
87 changed files with 4431 additions and 735 deletions

View File

@ -0,0 +1,13 @@
import React from 'react'
export const SendIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
width="19px"
{...props}
>
<title>Send</title>
<path d="M476.59 227.05l-.16-.07L49.35 49.84A23.56 23.56 0 0027.14 52 24.65 24.65 0 0016 72.59v113.29a24 24 0 0019.52 23.57l232.93 43.07a4 4 0 010 7.86L35.53 303.45A24 24 0 0016 327v113.31A23.57 23.57 0 0026.59 460a23.94 23.94 0 0013.22 4 24.55 24.55 0 009.52-1.93L476.4 285.94l.19-.09a32 32 0 000-58.8z" />
</svg>
)

View File

@ -0,0 +1,384 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--typebot-container-bg-image: none;
--typebot-container-bg-color: #f7f8ff;
--typebot-container-font-family: 'Inter';
--typebot-chat-view-max-width: 700px;
--typebot-chat-view-bg-color: #ffffff;
--typebot-chat-view-color: #303235;
--typebot-button-active-bg-color: #0042da;
--typebot-button-active-color: #ffffff;
--typebot-button-inactive-bg-color: #edf2f7;
--typebot-button-inactive-color: #303235;
--typebot-button-border: 1px solid var(--typebot-button-active-bg-color);
--typebot-button-shadow: none;
--typebot-host-bubble-bg-color: #f7f8ff;
--typebot-host-bubble-color: #303235;
--typebot-host-bubble-border: 1px solid var(--typebot-host-bubble-bg-color);
--typebot-host-bubble-shadow: none;
--typebot-guest-bubble-bg-color: #ff8e21;
--typebot-guest-bubble-color: #ffffff;
--typebot-guest-bubble-border: 1px solid var(--typebot-guest-bubble-bg-color);
--typebot-guest-bubble-shadow: none;
--typebot-input-bg-color: #ffffff;
--typebot-input-color: #303235;
--typebot-input-border: 1px solid var(--typebot-input-bg-color);
--typebot-input-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
--typebot-input-placeholder-color: #9095a0;
--typebot-header-bg-color: #ffffff;
--typebot-header-color: #303235;
--typebot-header-border: none;
--typebot-header-shadow: none;
--typebot-header-max-width: 1000px;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollable-container::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollable-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.StripeElement {
box-sizing: border-box;
height: 40px;
padding: 10px 12px;
}
/* Transitions */
.bubble-enter {
opacity: 0;
}
.bubble-enter-active {
opacity: 1;
transition-property: opacity;
transition-duration: 500ms;
transition-timing-function: ease-out;
}
.bubble-exit {
opacity: 1;
}
.bubble-exit-active {
opacity: 0;
transition-delay: 0ms !important;
transition-property: opacity;
transition-duration: 400ms;
transition-timing-function: ease-out;
}
.bubble-typing {
transition: width 400ms ease-out, height 400ms ease-out;
}
.content-opacity {
transition: opacity 400ms ease-in 200ms;
}
.bubble1,
.bubble2,
.bubble3 {
background-color: var(--typebot-host-bubble-color);
opacity: 0.5;
}
.bubble1 {
animation: chatBubbles 1s ease-in-out infinite;
}
.bubble2 {
animation: chatBubbles 1s ease-in-out infinite;
animation-delay: 0.3s;
}
.bubble3 {
animation: chatBubbles 1s ease-in-out infinite;
animation-delay: 0.5s;
}
@keyframes chatBubbles {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}
button,
input,
textarea {
font-weight: 300;
}
.slate-a {
text-decoration: underline;
}
.slate-html-container > div {
min-height: 24px;
}
.slate-bold {
font-weight: bold;
}
.slate-italic {
font-style: oblique;
}
.slate-underline {
text-decoration: underline;
}
.comp-input::-webkit-input-placeholder {
/* Chrome/Opera/Safari */
color: var(--typebot-input-placeholder-color) !important;
opacity: 1 !important;
}
.comp-input::-moz-placeholder {
/* Firefox 19+ */
color: var(--typebot-input-placeholder-color) !important;
opacity: 1 !important;
}
.comp-input::placeholder {
color: var(--typebot-input-placeholder-color) !important;
opacity: 1 !important;
}
.PhoneInput {
/* This is done to stretch the contents of this component. */
display: flex;
align-items: center;
}
.PhoneInput > input {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
}
.PhoneInput > input::placeholder {
color: var(--typebot-input-placeholder-color) !important;
}
.PhoneInputInput {
/* The phone number input stretches to fill all empty space */
flex: 1;
/* The phone number input should shrink
to make room for the extension input */
min-width: 0;
padding: 1rem 1rem 1rem 0;
}
.PhoneInputInput:focus {
outline: none;
}
.PhoneInputCountryIcon {
width: calc(
var(--PhoneInputCountryFlag-height) *
var(--PhoneInputCountryFlag-aspectRatio)
);
height: var(--PhoneInputCountryFlag-height);
box-shadow: none;
}
.PhoneInputCountryIcon--square {
width: var(--PhoneInputCountryFlag-height);
}
.PhoneInputCountryIcon--border {
/* Removed `background-color` because when an `<img/>` was still loading
it would show a dark gray rectangle. */
/* For some reason the `<img/>` is not stretched to 100% width and height
and sometime there can be seen white pixels of the background at top and bottom. */
background-color: var(--PhoneInputCountryFlag-backgroundColor--loading);
/* Border is added via `box-shadow` because `border` interferes with `width`/`height`. */
/* For some reason the `<img/>` is not stretched to 100% width and height
and sometime there can be seen white pixels of the background at top and bottom,
so an additional "inset" border is added. */
box-shadow: 0 0 0 var(--PhoneInputCountryFlag-borderWidth)
var(--PhoneInputCountryFlag-borderColor),
inset 0 0 0 var(--PhoneInputCountryFlag-borderWidth)
var(--PhoneInputCountryFlag-borderColor);
}
.PhoneInputCountryIconImg {
/* Fixes weird vertical space above the flag icon. */
/* https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/7#note_348586559 */
display: block;
/* 3rd party <SVG/> flag icons won't stretch if they have `width` and `height`.
Also, if an <SVG/> icon's aspect ratio was different, it wouldn't fit too. */
width: 100%;
height: 100%;
border-radius: 5px;
border: none;
}
.PhoneInputInternationalIconPhone {
opacity: var(--PhoneInputInternationalIconPhone-opacity);
}
.PhoneInputInternationalIconGlobe {
opacity: var(--PhoneInputInternationalIconGlobe-opacity);
}
/* Styling native country `<select/>`. */
.PhoneInputCountry {
position: relative;
align-self: stretch;
display: flex;
align-items: center;
padding: 0 1rem;
margin-right: 0;
}
.PhoneInputCountrySelect {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1;
border: 0;
opacity: 0;
cursor: pointer;
}
.PhoneInputCountrySelect[disabled] {
cursor: default;
}
.PhoneInputCountrySelectArrow {
display: block;
content: '';
width: var(--PhoneInputCountrySelectArrow-width);
height: var(--PhoneInputCountrySelectArrow-width);
margin-left: var(--PhoneInputCountrySelectArrow-marginLeft);
border-style: solid;
border-color: var(--PhoneInputCountrySelectArrow-color);
border-top-width: 0;
border-bottom-width: var(--PhoneInputCountrySelectArrow-borderWidth);
border-left-width: 0;
border-right-width: var(--PhoneInputCountrySelectArrow-borderWidth);
transform: var(--PhoneInputCountrySelectArrow-transform);
opacity: var(--PhoneInputCountrySelectArrow-opacity);
}
.PhoneInputCountrySelect:focus
+ .PhoneInputCountryIcon
+ .PhoneInputCountrySelectArrow {
opacity: 1;
color: var(--PhoneInputCountrySelectArrow-color--focus);
}
.PhoneInputCountrySelect:focus + .PhoneInputCountryIcon--border {
box-shadow: none;
}
.PhoneInputCountrySelect:focus
+ .PhoneInputCountryIcon
.PhoneInputInternationalIconGlobe {
opacity: 1;
color: var(--PhoneInputCountrySelectArrow-color--focus);
}
.typebot-container {
background-image: var(--typebot-container-bg-image);
background-color: var(--typebot-container-bg-color);
font-family: var(--typebot-container-font-family);
}
.custom-header {
color: var(--typebot-header-color);
background-color: var(--typebot-header-bg-color);
border-bottom: var(--typebot-header-border);
box-shadow: var(--typebot-header-shadow);
}
.custom-header-content {
max-width: var(--typebot-header-max-width);
}
.typebot-chat-view {
max-width: var(--typebot-chat-view-max-width);
background-color: var(--typebot-chat-view-bg-color);
}
.typebot-button.active {
color: var(--typebot-button-active-color);
background-color: var(--typebot-button-active-bg-color);
}
.typebot-button {
color: var(--typebot-button-inactive-color);
background-color: var(--typebot-button-inactive-bg-color);
border: var(--typebot-button-border);
box-shadow: var(--typebot-button-shadow);
}
.typebot-host-bubble {
color: var(--typebot-host-bubble-color);
}
.typebot-host-bubble > .bubble-typing {
background-color: var(--typebot-host-bubble-bg-color);
border: var(--typebot-host-bubble-border);
box-shadow: var(--typebot-host-bubble-shadow);
}
.typebot-guest-bubble {
color: var(--typebot-guest-bubble-color);
background-color: var(--typebot-guest-bubble-bg-color);
border: var(--typebot-guest-bubble-border);
box-shadow: var(--typebot-guest-bubble-shadow);
}
.typebot-input {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
border: var(--typebot-input-border);
box-shadow: var(--typebot-input-shadow);
}
.typebot-button > .send-icon {
fill: var(--typebot-button-active-color);
}
.text-on-chat {
color: var(--typebot-chat-view-color);
}
.star-icon {
cursor: pointer;
fill: transparent;
stroke-width: 30px;
stroke: var(--typebot-button-active-bg-color);
}
.star-icon.active {
fill: var(--typebot-button-active-bg-color);
}
.scale-labels {
font-size: 13px;
color: var(--typebot-button-active-bg-color);
font-weight: 600;
padding-right: 0.5rem;
}

View File

@ -0,0 +1,50 @@
import React, { useEffect, useRef, useState } from 'react'
import { useTypebot } from '../../contexts/TypebotContext'
import { HostAvatar } from '../avatars/HostAvatar'
import { useFrame } from 'react-frame-component'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
export const AvatarSideContainer = () => {
const { lastBubblesTopOffset } = useHostAvatars()
const { typebot } = useTypebot()
const { window, document } = useFrame()
const [marginBottom, setMarginBottom] = useState(
window.innerWidth < 400 ? 38 : 48
)
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
const isMobile = window.innerWidth < 400
setMarginBottom(isMobile ? 38 : 48)
})
resizeObserver.observe(document.body)
}, [])
return (
<div className="flex w-6 xs:w-10 mr-2 flex-shrink-0 items-center">
<TransitionGroup>
{lastBubblesTopOffset
.filter((n) => n !== -1)
.map((topOffset, idx) => (
<CSSTransition
key={idx}
classNames="bubble"
timeout={500}
unmountOnExit
>
<div
className="fixed w-6 h-6 xs:w-10 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
style={{
top: `calc(${topOffset}px - ${marginBottom}px)`,
transition: 'top 500ms ease-out',
}}
>
<HostAvatar typebotName={typebot.name} />
</div>
</CSSTransition>
))}
</TransitionGroup>
</div>
)
}

View File

@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react'
import { Block, Step } from '../../models'
import { animateScroll as scroll } from 'react-scroll'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
type ChatBlockProps = {
block: Block
onBlockEnd: (nextBlockId: string) => void
}
export const ChatBlock = ({ block, onBlockEnd }: ChatBlockProps) => {
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
useEffect(() => {
setDisplayedSteps([block.steps[0]])
}, [])
useEffect(() => {
autoScrollToBottom()
}, [displayedSteps])
const autoScrollToBottom = () => {
scroll.scrollToBottom({
duration: 500,
containerId: 'scrollable-container',
})
}
const displayNextStep = () => {
const currentStep = [...displayedSteps].pop()
if (currentStep?.target?.blockId)
return onBlockEnd(currentStep?.target?.blockId)
const nextStep = block.steps[displayedSteps.length]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
}
return (
<div className="flex">
<HostAvatarsContext>
<AvatarSideContainer />
<div className="flex flex-col w-full">
<TransitionGroup>
{displayedSteps.map((step) => (
<CSSTransition
key={step.id}
classNames="bubble"
timeout={500}
unmountOnExit
>
<ChatStep step={step} onTransitionEnd={displayNextStep} />
</CSSTransition>
))}
</TransitionGroup>
</div>
</HostAvatarsContext>
</div>
)
}

View File

@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react'
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
import { Step } from '../../../models'
import { isTextInputStep, isTextStep } from '../../../services/utils'
import { GuestBubble } from './bubbles/GuestBubble'
import { HostMessageBubble } from './bubbles/HostMessageBubble'
import { TextInput } from './inputs/TextInput'
export const ChatStep = ({
step,
onTransitionEnd,
}: {
step: Step
onTransitionEnd: () => void
}) => {
if (isTextStep(step))
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
if (isTextInputStep(step)) return <InputChatStep onSubmit={onTransitionEnd} />
return <span>No step</span>
}
const InputChatStep = ({ onSubmit }: { onSubmit: () => void }) => {
const { addNewAvatarOffset } = useHostAvatars()
const [answer, setAnswer] = useState<string>()
useEffect(() => {
addNewAvatarOffset()
}, [])
const handleSubmit = (value: string) => {
setAnswer(value)
onSubmit()
}
if (answer) {
return <GuestBubble message={answer} />
}
return <TextInput onSubmit={handleSubmit} />
}

View File

@ -0,0 +1,20 @@
import React from 'react'
import { CSSTransition } from 'react-transition-group'
interface Props {
message: string
}
export const GuestBubble = ({ message }: Props): JSX.Element => {
return (
<CSSTransition classNames="bubble" timeout={1000}>
<div className="flex justify-end mb-2 items-center">
<div className="flex items-end w-11/12 lg:w-4/6 justify-end">
<div className="inline-flex px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble">
{message}
</div>
</div>
</div>
</CSSTransition>
)
}

View File

@ -0,0 +1,78 @@
import React, { useEffect, useRef, useState } from 'react'
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
import { StepType, TextStep } from '../../../../models'
import { TypingContent } from './TypingContent'
type HostMessageBubbleProps = {
step: TextStep
onTransitionEnd: () => void
}
export const showAnimationDuration = 400
export const mediaLoadingFallbackTimeout = 5000
export const HostMessageBubble = ({
step,
onTransitionEnd,
}: HostMessageBubbleProps) => {
const { updateLastAvatarOffset } = useHostAvatars()
const messageContainer = useRef<HTMLDivElement | null>(null)
const [isTyping, setIsTyping] = useState(true)
useEffect(() => {
const wordCount = step.content.plainText.match(/(\w+)/g)?.length ?? 0
const typedWordsPerMinute = 250
const typingTimeout = (wordCount / typedWordsPerMinute) * 60000
sendAvatarOffset()
setTimeout(() => {
onTypingEnd()
}, typingTimeout)
}, [])
const onTypingEnd = () => {
setIsTyping(false)
setTimeout(() => {
sendAvatarOffset()
onTransitionEnd()
}, showAnimationDuration)
}
const sendAvatarOffset = () => {
if (!messageContainer.current) return
const containerDimensions = messageContainer.current.getBoundingClientRect()
updateLastAvatarOffset(containerDimensions.top + containerDimensions.height)
}
return (
<div className="flex flex-col" ref={messageContainer}>
<div className="flex mb-2 w-full lg:w-11/12 items-center">
<div className={'flex relative z-10 items-start typebot-host-bubble'}>
<div
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing z-10 "
style={{
width: isTyping ? '4rem' : '100%',
height: isTyping ? '2rem' : '100%',
}}
>
{isTyping ? <TypingContent /> : <></>}
</div>
{step.type === StepType.TEXT && (
<p
style={{
textOverflow: 'ellipsis',
}}
className={
'overflow-hidden content-opacity z-50 mx-4 my-2 whitespace-pre-wrap slate-html-container ' +
(isTyping ? 'opacity-0 h-6' : 'opacity-100 h-full')
}
dangerouslySetInnerHTML={{
__html: step.content.html,
}}
/>
)}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
import React from 'react'
export const TypingContent = (): JSX.Element => (
<div className="flex items-center">
<div className="w-2 h-2 mr-1 rounded-full bubble1" />
<div className="w-2 h-2 mr-1 rounded-full bubble2" />
<div className="w-2 h-2 rounded-full bubble3" />
</div>
)

View File

@ -0,0 +1 @@
export { ChatStep } from './ChatStep'

View File

@ -0,0 +1,46 @@
import React, { FormEvent, useRef, useState } from 'react'
import { SendIcon } from '../../../../assets/icons'
type TextInputProps = {
onSubmit: (value: string) => void
}
export const TextInput = ({ onSubmit }: TextInputProps) => {
const inputRef = useRef(null)
const [inputValue, setInputValue] = useState('')
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (inputValue === '') return
onSubmit(inputValue)
}
return (
<div className="flex flex-col w-full lg:w-4/6">
<div className="flex items-center">
<form
className="flex items-center justify-between rounded-lg pr-2 typebot-input"
onSubmit={handleSubmit}
>
<input
ref={inputRef}
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
type="text"
placeholder={'Type your answer...'}
onChange={(e) => setInputValue(e.target.value)}
required
/>
<button
type="submit"
className={
'py-2 px-4 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 active'
}
>
<span className="hidden xs:flex">Submit</span>
<SendIcon className="send-icon flex xs:hidden" />
</button>
</form>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { ChatBlock } from './ChatBlock'

View File

@ -0,0 +1,54 @@
import React, { useEffect, useRef, useState } from 'react'
import { PublicTypebot } from '..'
import { Block } from '..'
import { ChatBlock } from './ChatBlock/ChatBlock'
export const ConversationContainer = ({
typebot,
onNewBlockVisisble,
}: {
typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void
}) => {
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
const [isConversationEnded, setIsConversationEnded] = useState(false)
const bottomAnchor = useRef<HTMLDivElement | null>(null)
const displayNextBlock = (blockId: string) => {
const nextBlock = typebot.blocks.find((b) => b.id === blockId)
if (!nextBlock) return
onNewBlockVisisble(blockId)
setDisplayedBlocks([...displayedBlocks, nextBlock])
}
useEffect(() => {
const firstBlockId = typebot.startBlock.steps[0].target?.blockId
if (firstBlockId) displayNextBlock(firstBlockId)
}, [])
return (
<div
className="overflow-y-scroll w-full lg:w-3/4 min-h-full rounded lg:px-5 px-3 pt-10 relative scrollable-container typebot-chat-view"
id="scrollable-container"
>
{displayedBlocks.map((block, idx) => (
<ChatBlock
key={block.id + idx}
block={block}
onBlockEnd={displayNextBlock}
/>
))}
{/* We use a block to simulate padding because it makes iOS scroll flicker */}
<div
className="w-full"
ref={bottomAnchor}
style={{
transition: isConversationEnded ? 'height 1s' : '',
height: isConversationEnded ? '5%' : '20%',
}}
/>
</div>
)
}

View File

@ -1,6 +1,38 @@
import React from 'react'
import { PublicTypebot } from 'db'
import { PublicTypebot } from '../models'
import { TypebotContext } from '../contexts/TypebotContext'
import Frame from 'react-frame-component'
//@ts-ignore
import style from '../assets/style.css'
import { ConversationContainer } from './ConversationContainer'
import { ResultContext } from '../contexts/ResultsContext'
export const TypebotViewer = (props: PublicTypebot) => {
return <div>{props.name}</div>
export type TypebotViewerProps = {
typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void
}
export const TypebotViewer = ({
typebot,
onNewBlockVisisble,
}: TypebotViewerProps) => {
return (
<Frame
id="typebot-iframe"
head={<style>{style}</style>}
style={{ width: '100%' }}
>
<TypebotContext typebot={typebot}>
<ResultContext typebotId={typebot.id}>
<div className="flex text-base overflow-hidden bg-cover h-screen w-screen typebot-container flex-col items-center">
<div className="flex w-full h-full justify-center">
<ConversationContainer
typebot={typebot}
onNewBlockVisisble={onNewBlockVisisble}
/>
</div>
</div>
</ResultContext>
</TypebotContext>
</Frame>
)
}

View File

@ -0,0 +1,60 @@
import React from 'react'
type DefaultAvatarProps = {
displayName?: string
size?: 'extra-small' | 'small' | 'medium' | 'large' | 'full'
className?: string
}
export const DefaultAvatar = ({
displayName,
}: DefaultAvatarProps): 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'
}
>
<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
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>
)

View File

@ -0,0 +1,14 @@
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>
)
}

View File

@ -0,0 +1,38 @@
import React, { createContext, ReactNode, useContext, useState } from 'react'
// This context just keeps track of the top offset of host avatar
const hostAvatarsContext = createContext<{
lastBubblesTopOffset: number[]
addNewAvatarOffset: () => void
updateLastAvatarOffset: (newOffset: number) => void
//@ts-ignore
}>({})
export const HostAvatarsContext = ({ children }: { children: ReactNode }) => {
const [lastBubblesTopOffset, setLastBubblesTopOffset] = useState<number[]>([
-1,
])
const updateLastAvatarOffset = (newOffset: number) => {
const offsets = [...lastBubblesTopOffset]
offsets[offsets.length - 1] = newOffset
setLastBubblesTopOffset(offsets)
}
const addNewAvatarOffset = () =>
setLastBubblesTopOffset([...lastBubblesTopOffset, -1])
return (
<hostAvatarsContext.Provider
value={{
lastBubblesTopOffset,
updateLastAvatarOffset,
addNewAvatarOffset,
}}
>
{children}
</hostAvatarsContext.Provider>
)
}
export const useHostAvatars = () => useContext(hostAvatarsContext)

View File

@ -0,0 +1,50 @@
import { Answer, Result } from '../models'
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react'
const resultContext = createContext<{
result: Result
setResult: Dispatch<SetStateAction<Result>>
addAnswer: (answer: Answer) => void
//@ts-ignore
}>({})
export const ResultContext = ({
children,
typebotId,
}: {
children: ReactNode
typebotId: string
}) => {
const [result, setResult] = useState<Result>({
id: 'tmp',
createdAt: new Date(),
updatedAt: new Date(),
answers: [],
typebotId,
isCompleted: false,
})
const addAnswer = (answer: Answer) =>
setResult({ ...result, answers: [...result.answers, answer] })
return (
<resultContext.Provider
value={{
result,
setResult,
addAnswer,
}}
>
{children}
</resultContext.Provider>
)
}
export const useResult = () => useContext(resultContext)

View File

@ -0,0 +1,27 @@
import React, { createContext, ReactNode, useContext } from 'react'
import { PublicTypebot } from '../models/publicTypebot'
const typebotContext = createContext<{
typebot: PublicTypebot
//@ts-ignore
}>({})
export const TypebotContext = ({
children,
typebot,
}: {
children: ReactNode
typebot: PublicTypebot
}) => {
return (
<typebotContext.Provider
value={{
typebot,
}}
>
{children}
</typebotContext.Provider>
)
}
export const useTypebot = () => useContext(typebotContext)

View File

@ -1 +1,3 @@
export * from './typebot'
export * from './publicTypebot'
export * from './result'

View File

@ -0,0 +1,10 @@
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
import { Block, StartBlock } from '.'
export type PublicTypebot = Omit<
PublicTypebotFromPrisma,
'blocks' | 'startBlock'
> & {
blocks: Block[]
startBlock: StartBlock
}

View File

@ -0,0 +1,11 @@
import { Result as ResultFromPrisma } from 'db'
export type Result = Omit<ResultFromPrisma, 'answers'> & {
answers: Answer[]
}
export type Answer = {
blockId: string
stepId: string
content: string
}

View File

@ -1,6 +1,6 @@
import { Typebot as TypebotFromPrisma } from 'db'
export type Typebot = TypebotFromPrisma & {
export type Typebot = Omit<TypebotFromPrisma, 'blocks' | 'startBlock'> & {
blocks: Block[]
startBlock: StartBlock
}
@ -36,38 +36,21 @@ export type Block = {
export enum StepType {
START = 'start',
TEXT = 'text',
IMAGE = 'image',
BUTTONS = 'buttons',
DATE_PICKER = 'date picker',
TEXT_INPUT = 'text input',
}
export type Target = { blockId: string; stepId?: string }
export type Step = { id: string; blockId: string; target?: Target } & (
| TextStep
| ImageStep
| ButtonsStep
| DatePickerStep
)
export type TextStep = {
export type Step = BubbleStep | InputStep
export type BubbleStep = TextStep
export type InputStep = TextInputStep
export type StepBase = { id: string; blockId: string; target?: Target }
export type TextStep = StepBase & {
type: StepType.TEXT
content: string
content: { html: string; richText: unknown[]; plainText: string }
}
export type ImageStep = {
type: StepType.IMAGE
content: { url: string }
}
export type ButtonsStep = {
type: StepType.BUTTONS
buttons: Button[]
}
export type DatePickerStep = {
type: StepType.DATE_PICKER
content: string
export type TextInputStep = StepBase & {
type: StepType.TEXT_INPUT
}
export type Button = {

View File

@ -0,0 +1,7 @@
import { Step, TextStep, StepType, TextInputStep } from '../models'
export const isTextStep = (step: Step): step is TextStep =>
step.type === StepType.TEXT
export const isTextInputStep = (step: Step): step is TextInputStep =>
step.type === StepType.TEXT_INPUT

View File

@ -1 +0,0 @@
/*! tailwindcss v3.0.5 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}