🦴 Add settings page backbone
This commit is contained in:
26
apps/builder/components/settings/SettingsContent.tsx
Normal file
26
apps/builder/components/settings/SettingsContent.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Flex, Stack } from '@chakra-ui/react'
|
||||||
|
import { TypingEmulationSettings } from 'bot-engine'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import React from 'react'
|
||||||
|
import { TypingEmulation } from './TypingEmulation'
|
||||||
|
|
||||||
|
export const SettingsContent = () => {
|
||||||
|
const { typebot, updateSettings } = useTypebot()
|
||||||
|
|
||||||
|
const handleTypingEmulationUpdate = (
|
||||||
|
typingEmulation: TypingEmulationSettings
|
||||||
|
) => {
|
||||||
|
if (!typebot) return
|
||||||
|
updateSettings({ ...typebot.settings, typingEmulation })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||||
|
<Stack p="6" rounded="md" borderWidth={1} w="600px" minH="500px" mt={10}>
|
||||||
|
<TypingEmulation
|
||||||
|
typingEmulation={typebot?.settings.typingEmulation}
|
||||||
|
onUpdate={handleTypingEmulationUpdate}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
apps/builder/components/settings/SmartNumberInput.tsx
Normal file
39
apps/builder/components/settings/SmartNumberInput.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
NumberInputProps,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export const SmartNumberInput = ({
|
||||||
|
initialValue,
|
||||||
|
onValueChange,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
initialValue: number
|
||||||
|
onValueChange: (value: number) => void
|
||||||
|
} & NumberInputProps) => {
|
||||||
|
const [value, setValue] = useState(initialValue.toString())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value.endsWith('.') || value.endsWith(',')) return
|
||||||
|
if (value === '') onValueChange(0)
|
||||||
|
const newValue = parseFloat(value)
|
||||||
|
if (isNaN(newValue)) return
|
||||||
|
onValueChange(newValue)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput onChange={setValue} value={value} {...props}>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
)
|
||||||
|
}
|
||||||
63
apps/builder/components/settings/TypingEmulation.tsx
Normal file
63
apps/builder/components/settings/TypingEmulation.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Flex, Stack, Switch, Text } from '@chakra-ui/react'
|
||||||
|
import { TypingEmulationSettings } from 'bot-engine'
|
||||||
|
import React from 'react'
|
||||||
|
import { SmartNumberInput } from './SmartNumberInput'
|
||||||
|
|
||||||
|
type TypingEmulationProps = {
|
||||||
|
typingEmulation?: TypingEmulationSettings
|
||||||
|
onUpdate: (typingEmulation: TypingEmulationSettings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypingEmulation = ({
|
||||||
|
typingEmulation,
|
||||||
|
onUpdate,
|
||||||
|
}: TypingEmulationProps) => {
|
||||||
|
const handleSwitchChange = () => {
|
||||||
|
if (!typingEmulation) return
|
||||||
|
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSpeedChange = (speed: number) => {
|
||||||
|
if (!typingEmulation) return
|
||||||
|
onUpdate({ ...typingEmulation, speed })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMaxDelayChange = (maxDelay: number) => {
|
||||||
|
if (!typingEmulation) return
|
||||||
|
onUpdate({ ...typingEmulation, maxDelay: maxDelay })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Flex justifyContent="space-between" align="center">
|
||||||
|
<Text>Typing emulation</Text>
|
||||||
|
<Switch
|
||||||
|
isChecked={typingEmulation?.enabled}
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{typingEmulation?.enabled && (
|
||||||
|
<Stack pl={10}>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Text>Words per minutes:</Text>
|
||||||
|
<SmartNumberInput
|
||||||
|
initialValue={typingEmulation.speed}
|
||||||
|
onValueChange={handleSpeedChange}
|
||||||
|
maxW="100px"
|
||||||
|
step={30}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Text>Max delay (in seconds):</Text>
|
||||||
|
<SmartNumberInput
|
||||||
|
initialValue={typingEmulation.maxDelay}
|
||||||
|
onValueChange={handleMaxDelayChange}
|
||||||
|
maxW="100px"
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -53,9 +53,9 @@ export const TypebotHeader = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as={NextChakraLink}
|
as={NextChakraLink}
|
||||||
href={`/typebots/${typebot?.id}/design`}
|
href={`/typebots/${typebot?.id}/settings`}
|
||||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
colorScheme={router.pathname.endsWith('settings') ? 'blue' : 'gray'}
|
||||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
variant={router.pathname.endsWith('settings') ? 'outline' : 'ghost'}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { useToast } from '@chakra-ui/react'
|
import { useToast } from '@chakra-ui/react'
|
||||||
import { Block, Step, StepType, Target, Theme, Typebot } from 'bot-engine'
|
import {
|
||||||
|
Block,
|
||||||
|
Settings,
|
||||||
|
Step,
|
||||||
|
StepType,
|
||||||
|
Target,
|
||||||
|
Theme,
|
||||||
|
Typebot,
|
||||||
|
} from 'bot-engine'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -47,6 +55,7 @@ const typebotContext = createContext<{
|
|||||||
}) => void
|
}) => void
|
||||||
undo: () => void
|
undo: () => void
|
||||||
updateTheme: (theme: Theme) => void
|
updateTheme: (theme: Theme) => void
|
||||||
|
updateSettings: (settings: Settings) => void
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@@ -270,6 +279,11 @@ export const TypebotContext = ({
|
|||||||
setLocalTypebot({ ...localTypebot, theme })
|
setLocalTypebot({ ...localTypebot, theme })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSettings = (settings: Settings) => {
|
||||||
|
if (!localTypebot) return
|
||||||
|
setLocalTypebot({ ...localTypebot, settings })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<typebotContext.Provider
|
<typebotContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -286,6 +300,7 @@ export const TypebotContext = ({
|
|||||||
removeBlock,
|
removeBlock,
|
||||||
undo,
|
undo,
|
||||||
updateTheme,
|
updateTheme,
|
||||||
|
updateSettings,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { BackgroundType, StartBlock, StepType, Theme } from 'bot-engine'
|
import {
|
||||||
|
BackgroundType,
|
||||||
|
Settings,
|
||||||
|
StartBlock,
|
||||||
|
StepType,
|
||||||
|
Theme,
|
||||||
|
} from 'bot-engine'
|
||||||
import { Typebot, User } from 'db'
|
import { Typebot, User } from 'db'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
@@ -44,8 +50,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
background: { type: BackgroundType.NONE, content: '#ffffff' },
|
background: { type: BackgroundType.NONE, content: '#ffffff' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
const settings: Settings = {
|
||||||
|
typingEmulation: {
|
||||||
|
enabled: true,
|
||||||
|
speed: 300,
|
||||||
|
maxDelay: 1.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
const typebot = await prisma.typebot.create({
|
const typebot = await prisma.typebot.create({
|
||||||
data: { ...data, ownerId: user.id, startBlock, theme },
|
data: { ...data, ownerId: user.id, startBlock, theme, settings },
|
||||||
})
|
})
|
||||||
return res.send(typebot)
|
return res.send(typebot)
|
||||||
}
|
}
|
||||||
|
|||||||
23
apps/builder/pages/typebots/[id]/settings.tsx
Normal file
23
apps/builder/pages/typebots/[id]/settings.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Flex } from '@chakra-ui/layout'
|
||||||
|
import withAuth from 'components/HOC/withUser'
|
||||||
|
import { Seo } from 'components/Seo'
|
||||||
|
import { SettingsContent } from 'components/settings/SettingsContent'
|
||||||
|
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||||
|
import { TypebotContext } from 'contexts/TypebotContext'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SettingsPage = () => {
|
||||||
|
const { query } = useRouter()
|
||||||
|
return (
|
||||||
|
<TypebotContext typebotId={query.id?.toString()}>
|
||||||
|
<Seo title="Settings" />
|
||||||
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
|
<TypebotHeader />
|
||||||
|
<SettingsContent />
|
||||||
|
</Flex>
|
||||||
|
</TypebotContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(SettingsPage)
|
||||||
@@ -156,4 +156,5 @@ export const parseTypebotToPublicTypebot = (
|
|||||||
startBlock: typebot.startBlock,
|
startBlock: typebot.startBlock,
|
||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
|
settings: typebot.settings,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
||||||
|
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||||
import { StepType, TextStep } from '../../../../models'
|
import { StepType, TextStep } from '../../../../models'
|
||||||
|
import { computeTypingTimeout } from '../../../../services/chat'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingContent } from './TypingContent'
|
||||||
|
|
||||||
type HostMessageBubbleProps = {
|
type HostMessageBubbleProps = {
|
||||||
@@ -16,15 +18,18 @@ export const HostMessageBubble = ({
|
|||||||
step,
|
step,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
}: HostMessageBubbleProps) => {
|
}: HostMessageBubbleProps) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const { typingEmulation } = typebot.settings
|
||||||
const { updateLastAvatarOffset } = useHostAvatars()
|
const { updateLastAvatarOffset } = useHostAvatars()
|
||||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||||
const [isTyping, setIsTyping] = useState(true)
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wordCount = step.content.plainText.match(/(\w+)/g)?.length ?? 0
|
|
||||||
const typedWordsPerMinute = 250
|
|
||||||
const typingTimeout = (wordCount / typedWordsPerMinute) * 60000
|
|
||||||
sendAvatarOffset()
|
sendAvatarOffset()
|
||||||
|
const typingTimeout = computeTypingTimeout(
|
||||||
|
step.content.plainText,
|
||||||
|
typingEmulation
|
||||||
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onTypingEnd()
|
onTypingEnd()
|
||||||
}, typingTimeout)
|
}, typingTimeout)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
||||||
import { Block, StartBlock, Theme } from '.'
|
import { Block, Settings, StartBlock, Theme } from '.'
|
||||||
|
|
||||||
export type PublicTypebot = Omit<
|
export type PublicTypebot = Omit<
|
||||||
PublicTypebotFromPrisma,
|
PublicTypebotFromPrisma,
|
||||||
'blocks' | 'startBlock' | 'theme'
|
'blocks' | 'startBlock' | 'theme' | 'settings'
|
||||||
> & {
|
> & {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
startBlock: StartBlock
|
startBlock: StartBlock
|
||||||
theme: Theme
|
theme: Theme
|
||||||
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { Typebot as TypebotFromPrisma } from 'db'
|
|||||||
|
|
||||||
export type Typebot = Omit<
|
export type Typebot = Omit<
|
||||||
TypebotFromPrisma,
|
TypebotFromPrisma,
|
||||||
'blocks' | 'startBlock' | 'theme'
|
'blocks' | 'startBlock' | 'theme' | 'settings'
|
||||||
> & {
|
> & {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
startBlock: StartBlock
|
startBlock: StartBlock
|
||||||
theme: Theme
|
theme: Theme
|
||||||
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StartBlock = {
|
export type StartBlock = {
|
||||||
@@ -74,3 +75,13 @@ export type Background = {
|
|||||||
type: BackgroundType
|
type: BackgroundType
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
typingEmulation: TypingEmulationSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypingEmulationSettings = {
|
||||||
|
enabled: boolean
|
||||||
|
speed: number
|
||||||
|
maxDelay: number
|
||||||
|
}
|
||||||
|
|||||||
15
packages/bot-engine/src/services/chat.ts
Normal file
15
packages/bot-engine/src/services/chat.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TypingEmulationSettings } from '../models'
|
||||||
|
|
||||||
|
export const computeTypingTimeout = (
|
||||||
|
bubbleContent: string,
|
||||||
|
typingSettings: TypingEmulationSettings
|
||||||
|
) => {
|
||||||
|
const wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
||||||
|
const typedWordsPerMinute = typingSettings.speed
|
||||||
|
let typingTimeout = typingSettings.enabled
|
||||||
|
? (wordCount / typedWordsPerMinute) * 60000
|
||||||
|
: 0
|
||||||
|
if (typingTimeout > typingSettings.maxDelay * 1000)
|
||||||
|
typingTimeout = typingSettings.maxDelay * 1000
|
||||||
|
return typingTimeout
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `settings` to the `PublicTypebot` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `settings` to the `Typebot` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PublicTypebot" ADD COLUMN "settings" JSONB NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ADD COLUMN "settings" JSONB NOT NULL;
|
||||||
@@ -90,6 +90,7 @@ model Typebot {
|
|||||||
blocks Json[]
|
blocks Json[]
|
||||||
startBlock Json
|
startBlock Json
|
||||||
theme Json
|
theme Json
|
||||||
|
settings Json
|
||||||
}
|
}
|
||||||
|
|
||||||
model PublicTypebot {
|
model PublicTypebot {
|
||||||
@@ -100,6 +101,7 @@ model PublicTypebot {
|
|||||||
blocks Json[]
|
blocks Json[]
|
||||||
startBlock Json
|
startBlock Json
|
||||||
theme Json
|
theme Json
|
||||||
|
settings Json
|
||||||
}
|
}
|
||||||
|
|
||||||
model Result {
|
model Result {
|
||||||
|
|||||||
Reference in New Issue
Block a user