feat(inputs): ✨ Add buttons input
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"db": "*",
|
||||
"fast-equals": "^2.0.4",
|
||||
"models": "*",
|
||||
"react-frame-component": "^5.2.1",
|
||||
"react-frame-component": "5.2.2-alpha.0",
|
||||
"react-phone-number-input": "^3.1.44",
|
||||
"react-scroll": "^1.8.4",
|
||||
"react-transition-group": "^4.4.2",
|
||||
|
||||
@@ -4,18 +4,21 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import { Step, Table } from 'models'
|
||||
import { ChoiceInputStep, Step } from 'models'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import { isChoiceInput } from 'utils'
|
||||
|
||||
type ChatBlockProps = {
|
||||
steps: Table<Step>
|
||||
stepIds: string[]
|
||||
onBlockEnd: (nextBlockId?: string) => void
|
||||
}
|
||||
|
||||
export const ChatBlock = ({ steps, onBlockEnd }: ChatBlockProps) => {
|
||||
export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedSteps([steps.byId[steps.allIds[0]]])
|
||||
setDisplayedSteps([typebot.steps.byId[stepIds[0]]])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,17 +32,36 @@ export const ChatBlock = ({ steps, onBlockEnd }: ChatBlockProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
const displayNextStep = () => {
|
||||
const displayNextStep = (answerContent?: string) => {
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (!currentStep) throw new Error('currentStep should exist')
|
||||
const isSingleChoiceStep =
|
||||
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||
if (isSingleChoiceStep)
|
||||
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
|
||||
if (
|
||||
currentStep?.target?.blockId ||
|
||||
displayedSteps.length === steps.allIds.length
|
||||
displayedSteps.length === stepIds.length
|
||||
)
|
||||
return onBlockEnd(currentStep?.target?.blockId)
|
||||
const nextStep = steps.byId[displayedSteps.length]
|
||||
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
}
|
||||
|
||||
const getSingleChoiceTargetId = (
|
||||
currentStep: ChoiceInputStep,
|
||||
answerContent?: string
|
||||
) => {
|
||||
const itemId = currentStep.options.itemIds.find(
|
||||
(itemId) => typebot.choiceItems.byId[itemId].content === answerContent
|
||||
)
|
||||
if (!itemId) throw new Error('itemId should exist')
|
||||
const targetId =
|
||||
typebot.choiceItems.byId[itemId].target?.blockId ??
|
||||
currentStep.target?.blockId
|
||||
return targetId
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<HostAvatarsContext>
|
||||
|
||||
@@ -7,19 +7,20 @@ import { HostMessageBubble } from './bubbles/HostMessageBubble'
|
||||
import { TextForm } from './inputs/TextForm'
|
||||
import { isInputStep, isTextBubbleStep } from 'utils'
|
||||
import { DateForm } from './inputs/DateForm'
|
||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||
|
||||
export const ChatStep = ({
|
||||
step,
|
||||
onTransitionEnd,
|
||||
}: {
|
||||
step: Step
|
||||
onTransitionEnd: () => void
|
||||
onTransitionEnd: (answerContent?: string) => void
|
||||
}) => {
|
||||
const { addAnswer } = useAnswers()
|
||||
|
||||
const handleInputSubmit = (content: string) => {
|
||||
addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
||||
onTransitionEnd()
|
||||
onTransitionEnd(content)
|
||||
}
|
||||
|
||||
if (isTextBubbleStep(step))
|
||||
@@ -60,5 +61,7 @@ const InputChatStep = ({
|
||||
return <TextForm step={step} onSubmit={handleSubmit} />
|
||||
case InputStepType.DATE:
|
||||
return <DateForm options={step.options} onSubmit={handleSubmit} />
|
||||
case InputStepType.CHOICE:
|
||||
return <ChoiceForm options={step.options} onSubmit={handleSubmit} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ChoiceInputOptions } from 'models'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { filterTable } from 'utils'
|
||||
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||
import { SendButton } from './SendButton'
|
||||
|
||||
type ChoiceFormProps = {
|
||||
options?: ChoiceInputOptions
|
||||
onSubmit: (value: string) => void
|
||||
}
|
||||
|
||||
export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const items = useMemo(
|
||||
() => filterTable(options?.itemIds ?? [], typebot.choiceItems),
|
||||
[]
|
||||
)
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
const handleClick = (itemId: string) => (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (options?.isMultipleChoice) toggleSelectedItemId(itemId)
|
||||
else onSubmit(items.byId[itemId].content ?? '')
|
||||
}
|
||||
|
||||
const toggleSelectedItemId = (itemId: string) => {
|
||||
const existingIndex = selectedIds.indexOf(itemId)
|
||||
if (existingIndex !== -1) {
|
||||
selectedIds.splice(existingIndex, 1)
|
||||
setSelectedIds([...selectedIds])
|
||||
} else {
|
||||
setSelectedIds([...selectedIds, itemId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () =>
|
||||
onSubmit(selectedIds.map((itemId) => items.byId[itemId].content).join(', '))
|
||||
|
||||
return (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-wrap">
|
||||
{options?.itemIds.map((itemId) => (
|
||||
<button
|
||||
role={options?.isMultipleChoice ? 'checkbox' : 'button'}
|
||||
onClick={handleClick(itemId)}
|
||||
className={
|
||||
'py-2 px-4 font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none mr-2 mb-2 typebot-button ' +
|
||||
(selectedIds.includes(itemId) || !options?.isMultipleChoice
|
||||
? 'active'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
{items.byId[itemId].content}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{selectedIds.length > 0 && (
|
||||
<SendButton label={options?.buttonLabel ?? 'Send'} />
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -62,6 +62,7 @@ export const DateForm = ({
|
||||
<SendButton
|
||||
label={labels?.button ?? 'Send'}
|
||||
isDisabled={inputValues.to === '' && inputValues.from === ''}
|
||||
className="my-2 ml-2"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SendIcon } from '../../../../assets/icons'
|
||||
|
||||
type SendButtonProps = {
|
||||
label: string
|
||||
isDisabled: boolean
|
||||
isDisabled?: boolean
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
||||
export const SendButton = ({
|
||||
@@ -14,11 +14,12 @@ export const SendButton = ({
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className={
|
||||
'my-2 ml-2 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'
|
||||
}
|
||||
disabled={isDisabled}
|
||||
{...props}
|
||||
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 ' +
|
||||
props.className
|
||||
}
|
||||
>
|
||||
<span className="hidden xs:flex">{label}</span>
|
||||
<SendIcon className="send-icon flex xs:hidden" />
|
||||
|
||||
@@ -41,6 +41,7 @@ export const TextForm = ({ step, onSubmit }: TextFormProps) => {
|
||||
<SendButton
|
||||
label={step.options?.labels?.button ?? 'Send'}
|
||||
isDisabled={inputValue === ''}
|
||||
className="my-2 ml-2"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ConversationContainer = ({
|
||||
{displayedBlocks.map((block, idx) => (
|
||||
<ChatBlock
|
||||
key={block.id + idx}
|
||||
steps={filterTable(block.stepIds, typebot.steps)}
|
||||
stepIds={block.stepIds}
|
||||
onBlockEnd={displayNextBlock}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -92,21 +92,23 @@ model Typebot {
|
||||
folder DashboardFolder? @relation(fields: [folderId], references: [id])
|
||||
blocks Json
|
||||
steps Json
|
||||
choiceItems Json
|
||||
theme Json
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
}
|
||||
|
||||
model PublicTypebot {
|
||||
id String @id @default(cuid())
|
||||
typebotId String @unique
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
blocks Json
|
||||
steps Json
|
||||
theme Json
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
id String @id @default(cuid())
|
||||
typebotId String @unique
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
blocks Json
|
||||
steps Json
|
||||
choiceItems Json
|
||||
theme Json
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
}
|
||||
|
||||
model Result {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
||||
import { Block, Settings, Step, Theme } from './typebot'
|
||||
import { Block, ChoiceItem, Settings, Step, Theme } from './typebot'
|
||||
import { Table } from './utils'
|
||||
|
||||
export type PublicTypebot = Omit<
|
||||
@@ -8,6 +8,7 @@ export type PublicTypebot = Omit<
|
||||
> & {
|
||||
blocks: Table<Block>
|
||||
steps: Table<Step>
|
||||
choiceItems: Table<ChoiceItem>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Target } from '.'
|
||||
import { StepBase } from './steps'
|
||||
|
||||
export type InputStep =
|
||||
@@ -7,6 +8,7 @@ export type InputStep =
|
||||
| UrlInputStep
|
||||
| DateInputStep
|
||||
| PhoneNumberInputStep
|
||||
| ChoiceInputStep
|
||||
|
||||
export enum InputStepType {
|
||||
TEXT = 'text input',
|
||||
@@ -15,6 +17,7 @@ export enum InputStepType {
|
||||
URL = 'url input',
|
||||
DATE = 'date input',
|
||||
PHONE = 'phone number input',
|
||||
CHOICE = 'choice input',
|
||||
}
|
||||
|
||||
export type TextInputStep = StepBase & {
|
||||
@@ -44,7 +47,25 @@ export type DateInputStep = StepBase & {
|
||||
|
||||
export type PhoneNumberInputStep = StepBase & {
|
||||
type: InputStepType.PHONE
|
||||
options?: InputOptionsBase
|
||||
options?: InputTextOptionsBase
|
||||
}
|
||||
|
||||
export type ChoiceInputStep = StepBase & {
|
||||
type: InputStepType.CHOICE
|
||||
options: ChoiceInputOptions
|
||||
}
|
||||
|
||||
export type ChoiceInputOptions = {
|
||||
itemIds: string[]
|
||||
isMultipleChoice?: boolean
|
||||
buttonLabel?: string
|
||||
}
|
||||
|
||||
export type ChoiceItem = {
|
||||
id: string
|
||||
stepId: string
|
||||
content?: string
|
||||
target?: Target
|
||||
}
|
||||
|
||||
export type DateInputOptions = {
|
||||
@@ -53,19 +74,19 @@ export type DateInputOptions = {
|
||||
isRange?: boolean
|
||||
}
|
||||
|
||||
export type EmailInputOptions = InputOptionsBase
|
||||
export type EmailInputOptions = InputTextOptionsBase
|
||||
|
||||
export type UrlInputOptions = InputOptionsBase
|
||||
export type UrlInputOptions = InputTextOptionsBase
|
||||
|
||||
type InputOptionsBase = {
|
||||
type InputTextOptionsBase = {
|
||||
labels?: { placeholder?: string; button?: string }
|
||||
}
|
||||
|
||||
export type TextInputOptions = InputOptionsBase & {
|
||||
export type TextInputOptions = InputTextOptionsBase & {
|
||||
isLong?: boolean
|
||||
}
|
||||
|
||||
export type NumberInputOptions = InputOptionsBase & {
|
||||
export type NumberInputOptions = InputTextOptionsBase & {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Typebot as TypebotFromPrisma } from 'db'
|
||||
import { ChoiceItem } from './steps/inputs'
|
||||
import { Table } from '../utils'
|
||||
import { Settings } from './settings'
|
||||
import { Step } from './steps/steps'
|
||||
@@ -10,6 +11,7 @@ export type Typebot = Omit<
|
||||
> & {
|
||||
blocks: Table<Block>
|
||||
steps: Table<Step>
|
||||
choiceItems: Table<ChoiceItem>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BubbleStepType,
|
||||
ChoiceInputStep,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
Step,
|
||||
@@ -49,3 +50,9 @@ export const isTextBubbleStep = (step: Step): step is TextStep =>
|
||||
|
||||
export const isTextInputStep = (step: Step): step is TextInputStep =>
|
||||
step.type === InputStepType.TEXT
|
||||
|
||||
export const isChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||
step.type === InputStepType.CHOICE
|
||||
|
||||
export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||
step.type === InputStepType.CHOICE && !step.options.isMultipleChoice
|
||||
|
||||
Reference in New Issue
Block a user