⚡ (openai) Add Messages sequence type
To make it easy to just plug a sequence of user / assistant messages to Chat completion task Closes #387
This commit is contained in:
@ -24,7 +24,7 @@ import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||
|
||||
type Props = {
|
||||
initialVariableId?: string
|
||||
initialVariableId: string | undefined
|
||||
autoFocus?: boolean
|
||||
onSelectVariable: (
|
||||
variable: Pick<Variable, 'id' | 'name'> | undefined
|
||||
|
@ -164,6 +164,7 @@ const TextBubbleEditorContent = ({
|
||||
zIndex={10}
|
||||
>
|
||||
<VariableSearchInput
|
||||
initialVariableId={undefined}
|
||||
onSelectVariable={handleVariableSelected}
|
||||
placeholder="Search for a variable"
|
||||
autoFocus
|
||||
|
@ -1,6 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
export const DateNodeContent = () => (
|
||||
<Text color={'gray.500'}>Pick a date...</Text>
|
||||
)
|
||||
type Props = {
|
||||
variableId?: string
|
||||
}
|
||||
export const DateNodeContent = ({ variableId }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text color={'gray.500'}>Pick a date...</Text>
|
||||
)
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { EmailInputBlock } from 'models'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
type Props = {
|
||||
variableId?: string
|
||||
placeholder: EmailInputBlock['options']['labels']['placeholder']
|
||||
}
|
||||
|
||||
export const EmailInputNodeContent = ({ placeholder }: Props) => (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
export const EmailInputNodeContent = ({ variableId, placeholder }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { FileInputOptions } from 'models'
|
||||
|
||||
@ -5,8 +6,13 @@ type Props = {
|
||||
options: FileInputOptions
|
||||
}
|
||||
|
||||
export const FileInputContent = ({ options: { isMultipleAllowed } }: Props) => (
|
||||
<Text noOfLines={1} pr="6">
|
||||
Collect {isMultipleAllowed ? 'files' : 'file'}
|
||||
</Text>
|
||||
)
|
||||
export const FileInputContent = ({
|
||||
options: { isMultipleAllowed, variableId },
|
||||
}: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text noOfLines={1} pr="6">
|
||||
Collect {isMultipleAllowed ? 'files' : 'file'}
|
||||
</Text>
|
||||
)
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { NumberInputBlock } from 'models'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
type Props = {
|
||||
variableId?: string
|
||||
placeholder: NumberInputBlock['options']['labels']['placeholder']
|
||||
}
|
||||
|
||||
export const NumberNodeContent = ({ placeholder }: Props) => (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
export const NumberNodeContent = ({ variableId, placeholder }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { PhoneNumberInputOptions } from 'models'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
type Props = {
|
||||
variableId?: string
|
||||
placeholder: PhoneNumberInputOptions['labels']['placeholder']
|
||||
}
|
||||
|
||||
export const PhoneNodeContent = ({ placeholder }: Props) => (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
export const PhoneNodeContent = ({ variableId, placeholder }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { RatingInputBlock } from 'models'
|
||||
|
||||
type Props = {
|
||||
variableId?: string
|
||||
block: RatingInputBlock
|
||||
}
|
||||
|
||||
export const RatingInputContent = ({ block }: Props) => (
|
||||
<Text noOfLines={1} pr="6">
|
||||
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
|
||||
{block.options.length}
|
||||
</Text>
|
||||
)
|
||||
export const RatingInputContent = ({ variableId, block }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text noOfLines={1} pr="6">
|
||||
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
|
||||
{block.options.length}
|
||||
</Text>
|
||||
)
|
||||
|
@ -33,9 +33,9 @@ test('options should work', async ({ page }) => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(page.locator(`text=Send`)).toBeHidden()
|
||||
await page.locator(`text=8`).click()
|
||||
await page.getByRole('button', { name: '8' }).click()
|
||||
await page.locator(`text=Send`).click()
|
||||
await expect(page.locator(`text=8`)).toBeVisible()
|
||||
await expect(page.getByTestId('guest-bubble')).toHaveText('8')
|
||||
await page.click('text=Rate from 0 to 10')
|
||||
await page.click('text="10"')
|
||||
await page.click('text="5"')
|
||||
@ -47,10 +47,8 @@ test('options should work', async ({ page }) => {
|
||||
await page.fill('[placeholder="Not likely at all"]', 'Not likely at all')
|
||||
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
|
||||
await page.click('text="Restart"')
|
||||
await expect(page.locator(`text=8`)).toBeHidden()
|
||||
await expect(page.locator(`text=4`)).toBeHidden()
|
||||
await expect(page.locator(`text=Not likely at all`)).toBeVisible()
|
||||
await expect(page.locator(`text=Extremely likely`)).toBeVisible()
|
||||
await page.locator(`svg >> nth=4`).click()
|
||||
await expect(page.locator(`text=5`)).toBeVisible()
|
||||
await page.locator('typebot-standard').locator(`svg >> nth=4`).click()
|
||||
await expect(page.locator('typebot-standard').locator(`text=5`)).toBeVisible()
|
||||
})
|
||||
|
@ -1,14 +1,29 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { TextInputOptions } from 'models'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
type Props = {
|
||||
placeholder: TextInputOptions['labels']['placeholder']
|
||||
isLong: TextInputOptions['isLong']
|
||||
variableId?: string
|
||||
}
|
||||
|
||||
export const TextInputNodeContent = ({ placeholder, isLong }: Props) => (
|
||||
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)
|
||||
export const TextInputNodeContent = ({
|
||||
placeholder,
|
||||
isLong,
|
||||
variableId,
|
||||
}: Props) => {
|
||||
if (variableId)
|
||||
return (
|
||||
<WithVariableContent
|
||||
variableId={variableId}
|
||||
h={isLong ? '100px' : 'auto'}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { UrlInputOptions } from 'models'
|
||||
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||
|
||||
type Props = {
|
||||
variableId?: string
|
||||
placeholder: UrlInputOptions['labels']['placeholder']
|
||||
}
|
||||
|
||||
export const UrlNodeContent = ({ placeholder }: Props) => (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
export const UrlNodeContent = ({ placeholder, variableId }: Props) =>
|
||||
variableId ? (
|
||||
<WithVariableContent variableId={variableId} />
|
||||
) : (
|
||||
<Text color={'gray.500'}>{placeholder}</Text>
|
||||
)
|
||||
|
@ -1,36 +1,88 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { TableListItemProps } from '@/components/TableList'
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { Variable } from 'models'
|
||||
import {
|
||||
chatCompletionMessageCustomRoles,
|
||||
chatCompletionMessageRoles,
|
||||
ChatCompletionOpenAIOptions,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
|
||||
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
|
||||
|
||||
const roles = [
|
||||
...chatCompletionMessageCustomRoles,
|
||||
...chatCompletionMessageRoles,
|
||||
]
|
||||
|
||||
export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
|
||||
const changeRole = (role: (typeof chatCompletionMessageRoles)[number]) => {
|
||||
onItemChange({ ...item, role })
|
||||
const changeRole = (role: (typeof roles)[number]) => {
|
||||
onItemChange({
|
||||
...item,
|
||||
role,
|
||||
content: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const changeContent = (content: string) => {
|
||||
const changeSingleMessageContent = (content: string) => {
|
||||
if (item.role === 'Messages sequence ✨') return
|
||||
onItemChange({ ...item, content })
|
||||
}
|
||||
|
||||
const changeAssistantVariableId = (
|
||||
variable: Pick<Variable, 'id'> | undefined
|
||||
) => {
|
||||
if (item.role !== 'Messages sequence ✨') return
|
||||
onItemChange({
|
||||
...item,
|
||||
content: {
|
||||
...item.content,
|
||||
assistantMessagesVariableId: variable?.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const changeUserVariableId = (variable: Pick<Variable, 'id'> | undefined) => {
|
||||
if (item.role !== 'Messages sequence ✨') return
|
||||
onItemChange({
|
||||
...item,
|
||||
content: {
|
||||
...item.content,
|
||||
userMessagesVariableId: variable?.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList
|
||||
currentItem={item.role}
|
||||
items={chatCompletionMessageRoles}
|
||||
items={roles}
|
||||
onItemSelect={changeRole}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue={item.content}
|
||||
onChange={changeContent}
|
||||
placeholder="Content"
|
||||
placeholder="Select type"
|
||||
/>
|
||||
{item.role === 'Messages sequence ✨' ? (
|
||||
<>
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.content?.assistantMessagesVariableId}
|
||||
onSelectVariable={changeAssistantVariableId}
|
||||
placeholder="Assistant messages variable"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.content?.userMessagesVariableId}
|
||||
onSelectVariable={changeUserVariableId}
|
||||
placeholder="User messages variable"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextInput
|
||||
defaultValue={item.content}
|
||||
onChange={changeSingleMessageContent}
|
||||
placeholder="Content"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -61,11 +61,6 @@ export const OpenAIChatCompletionSettings = ({
|
||||
</TextLink>{' '}
|
||||
to better understand the available options.
|
||||
</Text>
|
||||
<DropdownList
|
||||
currentItem={options.model}
|
||||
items={chatCompletionModels}
|
||||
onItemSelect={updateModel}
|
||||
/>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
@ -85,6 +80,21 @@ export const OpenAIChatCompletionSettings = ({
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Advanced settings
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<DropdownList
|
||||
currentItem={options.model}
|
||||
items={chatCompletionModels}
|
||||
onItemSelect={updateModel}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
|
@ -27,11 +27,11 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Select task' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Create chat completion' }).click()
|
||||
await page.getByRole('button', { name: 'Messages' }).click()
|
||||
await page.getByRole('button', { name: 'Select role' }).click()
|
||||
await page.getByRole('button', { name: 'Select type' }).click()
|
||||
await page.getByRole('menuitem', { name: 'system' }).click()
|
||||
await page.getByPlaceholder('Content').first().fill('You are a helpful bot')
|
||||
await page.getByRole('button', { name: 'Add message' }).nth(1).click()
|
||||
await page.getByRole('button', { name: 'Select role' }).click()
|
||||
await page.getByRole('button', { name: 'Select type' }).click()
|
||||
await page.getByRole('menuitem', { name: 'assistant' }).click()
|
||||
await page.getByPlaceholder('Content').nth(1).fill('Hi there!')
|
||||
await page.getByRole('button', { name: 'Save answer' }).click()
|
||||
|
@ -24,7 +24,6 @@ import { WebhookContent } from '@/features/blocks/integrations/webhook'
|
||||
import { ChatwootBlockNodeLabel } from '@/features/blocks/integrations/chatwoot'
|
||||
import { RedirectNodeContent } from '@/features/blocks/logic/redirect'
|
||||
import { PabblyConnectContent } from '@/features/blocks/integrations/pabbly'
|
||||
import { WithVariableContent } from './WithVariableContent'
|
||||
import { PaymentInputContent } from '@/features/blocks/inputs/payment'
|
||||
import { RatingInputContent } from '@/features/blocks/inputs/rating'
|
||||
import { FileInputContent } from '@/features/blocks/inputs/fileUpload'
|
||||
@ -33,7 +32,6 @@ import { GoogleSheetsNodeContent } from '@/features/blocks/integrations/googleSh
|
||||
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
|
||||
import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
||||
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
||||
import { isInputBlock, isChoiceInput } from 'utils'
|
||||
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
|
||||
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
||||
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
||||
@ -47,14 +45,6 @@ type Props = {
|
||||
indices: BlockIndices
|
||||
}
|
||||
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
if (
|
||||
isInputBlock(block) &&
|
||||
!isChoiceInput(block) &&
|
||||
block.options.variableId
|
||||
) {
|
||||
return <WithVariableContent block={block} />
|
||||
}
|
||||
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
return <TextBubbleContent block={block} />
|
||||
@ -74,6 +64,7 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
case InputBlockType.TEXT: {
|
||||
return (
|
||||
<TextInputNodeContent
|
||||
variableId={block.options.variableId}
|
||||
placeholder={block.options.labels.placeholder}
|
||||
isLong={block.options.isLong}
|
||||
/>
|
||||
@ -81,31 +72,52 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
}
|
||||
case InputBlockType.NUMBER: {
|
||||
return (
|
||||
<NumberNodeContent placeholder={block.options.labels.placeholder} />
|
||||
<NumberNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.EMAIL: {
|
||||
return (
|
||||
<EmailInputNodeContent placeholder={block.options.labels.placeholder} />
|
||||
<EmailInputNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.URL: {
|
||||
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
|
||||
return (
|
||||
<UrlNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.CHOICE: {
|
||||
return <ButtonsBlockNode block={block} indices={indices} />
|
||||
}
|
||||
case InputBlockType.PHONE: {
|
||||
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
|
||||
return (
|
||||
<PhoneNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.DATE: {
|
||||
return <DateNodeContent />
|
||||
return <DateNodeContent variableId={block.options.variableId} />
|
||||
}
|
||||
case InputBlockType.PAYMENT: {
|
||||
return <PaymentInputContent block={block} />
|
||||
}
|
||||
case InputBlockType.RATING: {
|
||||
return <RatingInputContent block={block} />
|
||||
return (
|
||||
<RatingInputContent
|
||||
block={block}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.FILE: {
|
||||
return <FileInputContent options={block.options} />
|
||||
|
@ -1,21 +1,18 @@
|
||||
import { InputBlock } from 'models'
|
||||
import { chakra, Text } from '@chakra-ui/react'
|
||||
import { chakra, Text, TextProps } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
block: InputBlock
|
||||
}
|
||||
variableId: string
|
||||
} & TextProps
|
||||
|
||||
export const WithVariableContent = ({ block }: Props) => {
|
||||
export const WithVariableContent = ({ variableId, ...props }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName = typebot?.variables.find(
|
||||
byId(block.options.variableId)
|
||||
)?.name
|
||||
const variableName = typebot?.variables.find(byId(variableId))?.name
|
||||
|
||||
return (
|
||||
<Text w="calc(100% - 25px)">
|
||||
<Text w="calc(100% - 25px)" {...props}>
|
||||
Collect{' '}
|
||||
<chakra.span
|
||||
bgColor="orange.400"
|
||||
|
@ -233,7 +233,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
width: '100px',
|
||||
width: '50px',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
|
||||
<Portal containerRef={parentModalRef}>
|
||||
<PopoverContent w="full" ref={popoverRef}>
|
||||
<VariableSearchInput
|
||||
initialVariableId={undefined}
|
||||
onSelectVariable={(variable) => {
|
||||
onClose()
|
||||
if (variable) onSelectVariable(variable)
|
||||
|
Reference in New Issue
Block a user