2
0

Add "Generate variables" actions in AI blocks

Closes #1586
This commit is contained in:
Baptiste Arnaud
2024-06-18 12:13:00 +02:00
parent bec9cb68ca
commit 76fcf7ee93
25 changed files with 860 additions and 165 deletions

View File

@ -0,0 +1,173 @@
import {
HStack,
IconButton,
Wrap,
Text,
WrapItem,
Input,
} from '@chakra-ui/react'
import { useRef, useState } from 'react'
import { CloseIcon } from './icons'
import { colors } from '@/lib/theme'
import { AnimatePresence, motion } from 'framer-motion'
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
import { isEmpty } from '@typebot.io/lib/utils'
type Props = {
items?: string[]
placeholder?: string
onChange: (value: string[]) => void
}
export const TagsInput = ({ items, placeholder, onChange }: Props) => {
const inputRef = useRef<HTMLInputElement>(null)
const [inputValue, setInputValue] = useState('')
const [isFocused, setIsFocused] = useState(false)
const [focusedTagIndex, setFocusedTagIndex] = useState<number>()
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setFocusedTagIndex(undefined)
setInputValue(e.target.value)
if (e.target.value.length - inputValue.length > 0) {
const values = convertStrToList(e.target.value)
if (values.length > 1) {
onChange([...(items ?? []), ...convertStrToList(e.target.value)])
setInputValue('')
}
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!items) return
if (e.key === 'Backspace') {
if (focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
setFocusedTagIndex((idx) => idx! - 1)
}
removeItem(focusedTagIndex)
return
}
if (inputValue === '' && focusedTagIndex === undefined) {
setFocusedTagIndex(items?.length - 1)
return
}
}
if (e.key === 'ArrowLeft') {
if (focusedTagIndex !== undefined) {
if (focusedTagIndex === 0) return
setFocusedTagIndex(focusedTagIndex - 1)
return
}
if (inputRef.current?.selectionStart === 0 && items) {
setFocusedTagIndex(items.length - 1)
return
}
}
if (e.key === 'ArrowRight' && focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
setFocusedTagIndex(undefined)
return
}
setFocusedTagIndex(focusedTagIndex + 1)
}
}
const removeItem = (index: number) => {
if (!items) return
const newItems = [...items]
newItems.splice(index, 1)
onChange(newItems)
}
const addItem = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isEmpty(inputValue)) return
setInputValue('')
onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()])
}
return (
<Wrap
spacing={1}
borderWidth={1}
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
p="2"
rounded="md"
borderColor={isFocused ? 'blue.500' : 'gray.200'}
transitionProperty="box-shadow, border-color"
transitionDuration="150ms"
transitionTimingFunction="ease-in-out"
onClick={() => inputRef.current?.focus()}
onKeyDown={handleKeyDown}
>
<AnimatePresence mode="popLayout">
{items?.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, transform: 'translateY(5px)' }}
animate={{ opacity: 1, transform: 'translateY(0)' }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
>
<WrapItem>
<Tag
content={item}
onDeleteClick={() => removeItem(index)}
isFocused={focusedTagIndex === index}
/>
</WrapItem>
</motion.div>
))}
</AnimatePresence>
<WrapItem>
<form onSubmit={addItem}>
<Input
ref={inputRef}
h="24px"
p="0"
borderWidth={0}
focusBorderColor="transparent"
size="sm"
value={inputValue}
onChange={handleInputChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={items && items.length === 0 ? placeholder : undefined}
/>
</form>
</WrapItem>
</Wrap>
)
}
const Tag = ({
isFocused,
content,
onDeleteClick,
}: {
isFocused?: boolean
content: string
onDeleteClick: () => void
}) => (
<HStack
spacing={0.5}
borderWidth="1px"
pl="1"
rounded="sm"
maxW="100%"
borderColor={isFocused ? 'blue.500' : undefined}
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
>
<Text fontSize="sm" noOfLines={1}>
{content}
</Text>
<IconButton
size="xs"
icon={<CloseIcon />}
aria-label="Remove tag"
variant="ghost"
onClick={onDeleteClick}
/>
</HStack>
)

View File

@ -22,6 +22,7 @@ import { isEmpty } from '@typebot.io/lib'
import { useGraph } from '@/features/graph/providers/GraphProvider'
import { ButtonsItemSettings } from './ButtonsItemSettings'
import { useTranslate } from '@tolgee/react'
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
type Props = {
item: ButtonItem
@ -70,26 +71,18 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
const handleEditableChange = (val: string) => {
if (val.length - itemValue.length && val.endsWith('\n')) return
const splittedBreakLines = val.split('\n')
const splittedCommas = val.split(',')
const isPastingMultipleItems =
val.length - itemValue.length > 1 &&
(splittedBreakLines.length > 2 || splittedCommas.length > 2)
if (isPastingMultipleItems) {
const values =
splittedBreakLines.length > 2 ? splittedBreakLines : splittedCommas
return values.forEach((v, i) => {
if (i === 0) {
setItemValue(v)
} else {
createItem(
{ content: v.trim() },
{ ...indices, itemIndex: indices.itemIndex + i }
)
}
if (val.length - itemValue.length === 1) return setItemValue(val)
const values = convertStrToList(val)
if (values.length === 1) {
setItemValue(values[0])
} else {
values.forEach((v, i) => {
createItem(
{ content: v },
{ ...indices, itemIndex: indices.itemIndex + i }
)
})
}
setItemValue(val)
}
const handlePlusClick = () => {

View File

@ -24,10 +24,11 @@ import {
ForgedBlockDefinition,
ForgedBlock,
} from '@typebot.io/forge-repository/types'
import { PrimitiveList } from '@/components/PrimitiveList'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'
import { TagsInput } from '@/components/TagsInput'
import { PrimitiveList } from '@/components/PrimitiveList'
const mdComponents = {
a: ({ href, children }) => (
@ -316,28 +317,43 @@ const ZodArrayContent = ({
const type = schema._def.type._def.innerType?._def.typeName
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
return (
<Stack spacing={0}>
<Stack
spacing={0}
marginTop={layout?.mergeWithLastField ? '-3' : undefined}
>
{layout?.label && <FormLabel>{layout.label}</FormLabel>}
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<PrimitiveList
onItemsChange={(items) => {
onDataChange(items)
}}
initialItems={data}
addLabel={`Add ${layout?.itemLabel ?? ''}`}
>
{({ item, onItemChange }) => (
<ZodFieldLayout
schema={schema._def.type}
data={item}
blockDef={blockDef}
blockOptions={blockOptions}
isInAccordion={isInAccordion}
onDataChange={onItemChange}
width="full"
/>
)}
</PrimitiveList>
<Stack
p="4"
rounded="md"
flex="1"
borderWidth="1px"
borderTopWidth={layout?.mergeWithLastField ? '0' : undefined}
borderTopRadius={layout?.mergeWithLastField ? '0' : undefined}
pt={layout?.mergeWithLastField ? '5' : undefined}
>
{type === 'ZodString' ? (
<TagsInput items={data} onChange={onDataChange} />
) : (
<PrimitiveList
onItemsChange={(items) => {
onDataChange(items)
}}
initialItems={data}
addLabel={`Add ${layout?.itemLabel ?? ''}`}
>
{({ item, onItemChange }) => (
<ZodFieldLayout
schema={schema._def.type}
data={item}
blockDef={blockDef}
blockOptions={blockOptions}
isInAccordion={isInAccordion}
onDataChange={onItemChange}
width="full"
/>
)}
</PrimitiveList>
)}
</Stack>
</Stack>
)

View File

@ -2,10 +2,9 @@ import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { isDefined } from '@typebot.io/lib'
import { TextInput } from '@/components/inputs'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { PrimitiveList } from '@/components/PrimitiveList'
import { TagsInput } from '@/components/TagsInput'
import { env } from '@typebot.io/env'
type Props = {
security: Settings['security']
@ -30,20 +29,11 @@ export const SecurityForm = ({ security, onUpdate }: Props) => {
By default your bot can be executed on any website.
</MoreInfoTooltip>
</FormLabel>
<PrimitiveList
initialItems={security?.allowedOrigins}
onItemsChange={updateItems}
addLabel="Add URL"
>
{({ item, onItemChange }) => (
<TextInput
width="full"
defaultValue={item}
onChange={onItemChange}
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
/>
)}
</PrimitiveList>
<TagsInput
items={security?.allowedOrigins}
onChange={updateItems}
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
/>
</FormControl>
</Stack>
)