⚡ (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'
|
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialVariableId?: string
|
initialVariableId: string | undefined
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean
|
||||||
onSelectVariable: (
|
onSelectVariable: (
|
||||||
variable: Pick<Variable, 'id' | 'name'> | undefined
|
variable: Pick<Variable, 'id' | 'name'> | undefined
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const TextBubbleEditorContent = ({
|
|||||||
zIndex={10}
|
zIndex={10}
|
||||||
>
|
>
|
||||||
<VariableSearchInput
|
<VariableSearchInput
|
||||||
|
initialVariableId={undefined}
|
||||||
onSelectVariable={handleVariableSelected}
|
onSelectVariable={handleVariableSelected}
|
||||||
placeholder="Search for a variable"
|
placeholder="Search for a variable"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
export const DateNodeContent = () => (
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
|
}
|
||||||
|
export const DateNodeContent = ({ variableId }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text color={'gray.500'}>Pick a date...</Text>
|
<Text color={'gray.500'}>Pick a date...</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { EmailInputBlock } from 'models'
|
import { EmailInputBlock } from 'models'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
placeholder: EmailInputBlock['options']['labels']['placeholder']
|
placeholder: EmailInputBlock['options']['labels']['placeholder']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmailInputNodeContent = ({ placeholder }: Props) => (
|
export const EmailInputNodeContent = ({ variableId, placeholder }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text color={'gray.500'}>{placeholder}</Text>
|
<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 { Text } from '@chakra-ui/react'
|
||||||
import { FileInputOptions } from 'models'
|
import { FileInputOptions } from 'models'
|
||||||
|
|
||||||
@@ -5,7 +6,12 @@ type Props = {
|
|||||||
options: FileInputOptions
|
options: FileInputOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileInputContent = ({ options: { isMultipleAllowed } }: Props) => (
|
export const FileInputContent = ({
|
||||||
|
options: { isMultipleAllowed, variableId },
|
||||||
|
}: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text noOfLines={1} pr="6">
|
<Text noOfLines={1} pr="6">
|
||||||
Collect {isMultipleAllowed ? 'files' : 'file'}
|
Collect {isMultipleAllowed ? 'files' : 'file'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { NumberInputBlock } from 'models'
|
import { NumberInputBlock } from 'models'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
placeholder: NumberInputBlock['options']['labels']['placeholder']
|
placeholder: NumberInputBlock['options']['labels']['placeholder']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberNodeContent = ({ placeholder }: Props) => (
|
export const NumberNodeContent = ({ variableId, placeholder }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text color={'gray.500'}>{placeholder}</Text>
|
<Text color={'gray.500'}>{placeholder}</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { PhoneNumberInputOptions } from 'models'
|
import { PhoneNumberInputOptions } from 'models'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
placeholder: PhoneNumberInputOptions['labels']['placeholder']
|
placeholder: PhoneNumberInputOptions['labels']['placeholder']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PhoneNodeContent = ({ placeholder }: Props) => (
|
export const PhoneNodeContent = ({ variableId, placeholder }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text color={'gray.500'}>{placeholder}</Text>
|
<Text color={'gray.500'}>{placeholder}</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { RatingInputBlock } from 'models'
|
import { RatingInputBlock } from 'models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
block: RatingInputBlock
|
block: RatingInputBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingInputContent = ({ block }: Props) => (
|
export const RatingInputContent = ({ variableId, block }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text noOfLines={1} pr="6">
|
<Text noOfLines={1} pr="6">
|
||||||
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
|
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
|
||||||
{block.options.length}
|
{block.options.length}
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('options should work', async ({ page }) => {
|
|||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Preview')
|
||||||
await expect(page.locator(`text=Send`)).toBeHidden()
|
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 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=Rate from 0 to 10')
|
||||||
await page.click('text="10"')
|
await page.click('text="10"')
|
||||||
await page.click('text="5"')
|
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="Not likely at all"]', 'Not likely at all')
|
||||||
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
|
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
|
||||||
await page.click('text="Restart"')
|
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=Not likely at all`)).toBeVisible()
|
||||||
await expect(page.locator(`text=Extremely likely`)).toBeVisible()
|
await expect(page.locator(`text=Extremely likely`)).toBeVisible()
|
||||||
await page.locator(`svg >> nth=4`).click()
|
await page.locator('typebot-standard').locator(`svg >> nth=4`).click()
|
||||||
await expect(page.locator(`text=5`)).toBeVisible()
|
await expect(page.locator('typebot-standard').locator(`text=5`)).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { TextInputOptions } from 'models'
|
import { TextInputOptions } from 'models'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
placeholder: TextInputOptions['labels']['placeholder']
|
placeholder: TextInputOptions['labels']['placeholder']
|
||||||
isLong: TextInputOptions['isLong']
|
isLong: TextInputOptions['isLong']
|
||||||
|
variableId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInputNodeContent = ({ placeholder, isLong }: Props) => (
|
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'}>
|
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
import { UrlInputOptions } from 'models'
|
import { UrlInputOptions } from 'models'
|
||||||
|
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
variableId?: string
|
||||||
placeholder: UrlInputOptions['labels']['placeholder']
|
placeholder: UrlInputOptions['labels']['placeholder']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UrlNodeContent = ({ placeholder }: Props) => (
|
export const UrlNodeContent = ({ placeholder, variableId }: Props) =>
|
||||||
|
variableId ? (
|
||||||
|
<WithVariableContent variableId={variableId} />
|
||||||
|
) : (
|
||||||
<Text color={'gray.500'}>{placeholder}</Text>
|
<Text color={'gray.500'}>{placeholder}</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,36 +1,88 @@
|
|||||||
import { DropdownList } from '@/components/DropdownList'
|
import { DropdownList } from '@/components/DropdownList'
|
||||||
import { TextInput } from '@/components/inputs'
|
import { TextInput } from '@/components/inputs'
|
||||||
|
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||||
import { TableListItemProps } from '@/components/TableList'
|
import { TableListItemProps } from '@/components/TableList'
|
||||||
import { Stack } from '@chakra-ui/react'
|
import { Stack } from '@chakra-ui/react'
|
||||||
|
import { Variable } from 'models'
|
||||||
import {
|
import {
|
||||||
|
chatCompletionMessageCustomRoles,
|
||||||
chatCompletionMessageRoles,
|
chatCompletionMessageRoles,
|
||||||
ChatCompletionOpenAIOptions,
|
ChatCompletionOpenAIOptions,
|
||||||
} from 'models/features/blocks/integrations/openai'
|
} from 'models/features/blocks/integrations/openai'
|
||||||
|
|
||||||
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
|
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
|
||||||
|
|
||||||
|
const roles = [
|
||||||
|
...chatCompletionMessageCustomRoles,
|
||||||
|
...chatCompletionMessageRoles,
|
||||||
|
]
|
||||||
|
|
||||||
export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
|
export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
|
||||||
const changeRole = (role: (typeof chatCompletionMessageRoles)[number]) => {
|
const changeRole = (role: (typeof roles)[number]) => {
|
||||||
onItemChange({ ...item, role })
|
onItemChange({
|
||||||
|
...item,
|
||||||
|
role,
|
||||||
|
content: undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeContent = (content: string) => {
|
const changeSingleMessageContent = (content: string) => {
|
||||||
|
if (item.role === 'Messages sequence ✨') return
|
||||||
onItemChange({ ...item, content })
|
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 (
|
return (
|
||||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||||
<DropdownList
|
<DropdownList
|
||||||
currentItem={item.role}
|
currentItem={item.role}
|
||||||
items={chatCompletionMessageRoles}
|
items={roles}
|
||||||
onItemSelect={changeRole}
|
onItemSelect={changeRole}
|
||||||
placeholder="Select role"
|
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
|
<TextInput
|
||||||
defaultValue={item.content}
|
defaultValue={item.content}
|
||||||
onChange={changeContent}
|
onChange={changeSingleMessageContent}
|
||||||
placeholder="Content"
|
placeholder="Content"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ export const OpenAIChatCompletionSettings = ({
|
|||||||
</TextLink>{' '}
|
</TextLink>{' '}
|
||||||
to better understand the available options.
|
to better understand the available options.
|
||||||
</Text>
|
</Text>
|
||||||
<DropdownList
|
|
||||||
currentItem={options.model}
|
|
||||||
items={chatCompletionModels}
|
|
||||||
onItemSelect={updateModel}
|
|
||||||
/>
|
|
||||||
<Accordion allowToggle allowMultiple>
|
<Accordion allowToggle allowMultiple>
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<AccordionButton>
|
<AccordionButton>
|
||||||
@@ -85,6 +80,21 @@ export const OpenAIChatCompletionSettings = ({
|
|||||||
/>
|
/>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</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>
|
<AccordionItem>
|
||||||
<AccordionButton>
|
<AccordionButton>
|
||||||
<Text w="full" textAlign="left">
|
<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('button', { name: 'Select task' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Create chat completion' }).click()
|
await page.getByRole('menuitem', { name: 'Create chat completion' }).click()
|
||||||
await page.getByRole('button', { name: 'Messages' }).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.getByRole('menuitem', { name: 'system' }).click()
|
||||||
await page.getByPlaceholder('Content').first().fill('You are a helpful bot')
|
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: '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.getByRole('menuitem', { name: 'assistant' }).click()
|
||||||
await page.getByPlaceholder('Content').nth(1).fill('Hi there!')
|
await page.getByPlaceholder('Content').nth(1).fill('Hi there!')
|
||||||
await page.getByRole('button', { name: 'Save answer' }).click()
|
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 { ChatwootBlockNodeLabel } from '@/features/blocks/integrations/chatwoot'
|
||||||
import { RedirectNodeContent } from '@/features/blocks/logic/redirect'
|
import { RedirectNodeContent } from '@/features/blocks/logic/redirect'
|
||||||
import { PabblyConnectContent } from '@/features/blocks/integrations/pabbly'
|
import { PabblyConnectContent } from '@/features/blocks/integrations/pabbly'
|
||||||
import { WithVariableContent } from './WithVariableContent'
|
|
||||||
import { PaymentInputContent } from '@/features/blocks/inputs/payment'
|
import { PaymentInputContent } from '@/features/blocks/inputs/payment'
|
||||||
import { RatingInputContent } from '@/features/blocks/inputs/rating'
|
import { RatingInputContent } from '@/features/blocks/inputs/rating'
|
||||||
import { FileInputContent } from '@/features/blocks/inputs/fileUpload'
|
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 { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
|
||||||
import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
||||||
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
||||||
import { isInputBlock, isChoiceInput } from 'utils'
|
|
||||||
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
|
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
|
||||||
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
||||||
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
||||||
@@ -47,14 +45,6 @@ type Props = {
|
|||||||
indices: BlockIndices
|
indices: BlockIndices
|
||||||
}
|
}
|
||||||
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||||
if (
|
|
||||||
isInputBlock(block) &&
|
|
||||||
!isChoiceInput(block) &&
|
|
||||||
block.options.variableId
|
|
||||||
) {
|
|
||||||
return <WithVariableContent block={block} />
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case BubbleBlockType.TEXT: {
|
case BubbleBlockType.TEXT: {
|
||||||
return <TextBubbleContent block={block} />
|
return <TextBubbleContent block={block} />
|
||||||
@@ -74,6 +64,7 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
case InputBlockType.TEXT: {
|
case InputBlockType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<TextInputNodeContent
|
<TextInputNodeContent
|
||||||
|
variableId={block.options.variableId}
|
||||||
placeholder={block.options.labels.placeholder}
|
placeholder={block.options.labels.placeholder}
|
||||||
isLong={block.options.isLong}
|
isLong={block.options.isLong}
|
||||||
/>
|
/>
|
||||||
@@ -81,31 +72,52 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
case InputBlockType.NUMBER: {
|
case InputBlockType.NUMBER: {
|
||||||
return (
|
return (
|
||||||
<NumberNodeContent placeholder={block.options.labels.placeholder} />
|
<NumberNodeContent
|
||||||
|
placeholder={block.options.labels.placeholder}
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputBlockType.EMAIL: {
|
case InputBlockType.EMAIL: {
|
||||||
return (
|
return (
|
||||||
<EmailInputNodeContent placeholder={block.options.labels.placeholder} />
|
<EmailInputNodeContent
|
||||||
|
placeholder={block.options.labels.placeholder}
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputBlockType.URL: {
|
case InputBlockType.URL: {
|
||||||
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
|
return (
|
||||||
|
<UrlNodeContent
|
||||||
|
placeholder={block.options.labels.placeholder}
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case InputBlockType.CHOICE: {
|
case InputBlockType.CHOICE: {
|
||||||
return <ButtonsBlockNode block={block} indices={indices} />
|
return <ButtonsBlockNode block={block} indices={indices} />
|
||||||
}
|
}
|
||||||
case InputBlockType.PHONE: {
|
case InputBlockType.PHONE: {
|
||||||
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
|
return (
|
||||||
|
<PhoneNodeContent
|
||||||
|
placeholder={block.options.labels.placeholder}
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case InputBlockType.DATE: {
|
case InputBlockType.DATE: {
|
||||||
return <DateNodeContent />
|
return <DateNodeContent variableId={block.options.variableId} />
|
||||||
}
|
}
|
||||||
case InputBlockType.PAYMENT: {
|
case InputBlockType.PAYMENT: {
|
||||||
return <PaymentInputContent block={block} />
|
return <PaymentInputContent block={block} />
|
||||||
}
|
}
|
||||||
case InputBlockType.RATING: {
|
case InputBlockType.RATING: {
|
||||||
return <RatingInputContent block={block} />
|
return (
|
||||||
|
<RatingInputContent
|
||||||
|
block={block}
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case InputBlockType.FILE: {
|
case InputBlockType.FILE: {
|
||||||
return <FileInputContent options={block.options} />
|
return <FileInputContent options={block.options} />
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { InputBlock } from 'models'
|
import { chakra, Text, TextProps } from '@chakra-ui/react'
|
||||||
import { chakra, Text } from '@chakra-ui/react'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTypebot } from '@/features/editor'
|
import { useTypebot } from '@/features/editor'
|
||||||
import { byId } from 'utils'
|
import { byId } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: InputBlock
|
variableId: string
|
||||||
}
|
} & TextProps
|
||||||
|
|
||||||
export const WithVariableContent = ({ block }: Props) => {
|
export const WithVariableContent = ({ variableId, ...props }: Props) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const variableName = typebot?.variables.find(
|
const variableName = typebot?.variables.find(byId(variableId))?.name
|
||||||
byId(block.options.variableId)
|
|
||||||
)?.name
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text w="calc(100% - 25px)">
|
<Text w="calc(100% - 25px)" {...props}>
|
||||||
Collect{' '}
|
Collect{' '}
|
||||||
<chakra.span
|
<chakra.span
|
||||||
bgColor="orange.400"
|
bgColor="orange.400"
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ const NonMemoizedDraggableGroupNode = ({
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '10px',
|
top: '10px',
|
||||||
width: '100px',
|
width: '50px',
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
|
|||||||
<Portal containerRef={parentModalRef}>
|
<Portal containerRef={parentModalRef}>
|
||||||
<PopoverContent w="full" ref={popoverRef}>
|
<PopoverContent w="full" ref={popoverRef}>
|
||||||
<VariableSearchInput
|
<VariableSearchInput
|
||||||
|
initialVariableId={undefined}
|
||||||
onSelectVariable={(variable) => {
|
onSelectVariable={(variable) => {
|
||||||
onClose()
|
onClose()
|
||||||
if (variable) onSelectVariable(variable)
|
if (variable) onSelectVariable(variable)
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
With the OpenAI block, you can create a chat completion based on your user queries and display the answer back to your typebot.
|
With the OpenAI block, you can create a chat completion based on your user queries and display the answer back to your typebot.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src="/img/blocks/integrations/openai.png"
|
src="/img/blocks/integrations/openai/overview.png"
|
||||||
width="600"
|
width="600"
|
||||||
alt="OpenAI block"
|
alt="OpenAI block"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
This integration comes with a convenient message type called **Messages sequence ✨**. It allows you to directly pass a sequence of saved assistant / user messages:
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/blocks/integrations/openai/messages-sequence.png"
|
||||||
|
width="600"
|
||||||
|
alt="OpenAI messages sequence"
|
||||||
|
/>
|
||||||
|
|||||||
@@ -2957,6 +2957,8 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -2978,6 +2980,39 @@
|
|||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Messages sequence ✨"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"assistantMessagesVariableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userMessagesVariableId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"role"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"responseMapping": {
|
"responseMapping": {
|
||||||
|
|||||||
BIN
apps/docs/static/img/blocks/integrations/openai/messages-sequence.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/integrations/openai/messages-sequence.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
@@ -1,7 +1,14 @@
|
|||||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||||
|
import { saveErrorLog } from '@/features/logs/api'
|
||||||
|
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
|
||||||
import { parseVariables, updateVariables } from '@/features/variables/utils'
|
import { parseVariables, updateVariables } from '@/features/variables/utils'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { SessionState, VariableWithUnknowValue } from 'models'
|
import {
|
||||||
|
SessionState,
|
||||||
|
Variable,
|
||||||
|
VariableWithUnknowValue,
|
||||||
|
VariableWithValue,
|
||||||
|
} from 'models'
|
||||||
import {
|
import {
|
||||||
ChatCompletionOpenAIOptions,
|
ChatCompletionOpenAIOptions,
|
||||||
OpenAICredentials,
|
OpenAICredentials,
|
||||||
@@ -20,6 +27,7 @@ export const createChatCompletionOpenAI = async (
|
|||||||
const {
|
const {
|
||||||
typebot: { variables },
|
typebot: { variables },
|
||||||
} = state
|
} = state
|
||||||
|
let newSessionState = state
|
||||||
if (!options.credentialsId) return { outgoingEdgeId }
|
if (!options.credentialsId) return { outgoingEdgeId }
|
||||||
const credentials = await prisma.credentials.findUnique({
|
const credentials = await prisma.credentials.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -34,24 +42,23 @@ export const createChatCompletionOpenAI = async (
|
|||||||
const configuration = new Configuration({
|
const configuration = new Configuration({
|
||||||
apiKey,
|
apiKey,
|
||||||
})
|
})
|
||||||
|
const { variablesTransformedToList, messages } = parseMessages(variables)(
|
||||||
|
options.messages
|
||||||
|
)
|
||||||
|
if (variablesTransformedToList.length > 0)
|
||||||
|
newSessionState = await updateVariables(state)(variablesTransformedToList)
|
||||||
const openai = new OpenAIApi(configuration)
|
const openai = new OpenAIApi(configuration)
|
||||||
|
try {
|
||||||
const {
|
const {
|
||||||
data: { choices, usage },
|
data: { choices, usage },
|
||||||
} = await openai.createChatCompletion({
|
} = await openai.createChatCompletion({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
messages: options.messages
|
messages,
|
||||||
.map((message) => ({
|
|
||||||
role: message.role,
|
|
||||||
content: parseVariables(variables)(message.content),
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
(message) => isNotEmpty(message.role) && isNotEmpty(message.content)
|
|
||||||
) as ChatCompletionRequestMessage[],
|
|
||||||
})
|
})
|
||||||
const messageContent = choices[0].message?.content
|
const messageContent = choices[0].message?.content
|
||||||
const totalTokens = usage?.total_tokens
|
const totalTokens = usage?.total_tokens
|
||||||
if (!messageContent) {
|
if (!messageContent) {
|
||||||
return { outgoingEdgeId }
|
return { outgoingEdgeId, newSessionState }
|
||||||
}
|
}
|
||||||
const newVariables = options.responseMapping.reduce<
|
const newVariables = options.responseMapping.reduce<
|
||||||
VariableWithUnknowValue[]
|
VariableWithUnknowValue[]
|
||||||
@@ -61,7 +68,9 @@ export const createChatCompletionOpenAI = async (
|
|||||||
if (mapping.valueToExtract === 'Message content') {
|
if (mapping.valueToExtract === 'Message content') {
|
||||||
newVariables.push({
|
newVariables.push({
|
||||||
...existingVariable,
|
...existingVariable,
|
||||||
value: messageContent,
|
value: Array.isArray(existingVariable.value)
|
||||||
|
? existingVariable.value.concat(messageContent)
|
||||||
|
: messageContent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
|
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
|
||||||
@@ -73,7 +82,7 @@ export const createChatCompletionOpenAI = async (
|
|||||||
return newVariables
|
return newVariables
|
||||||
}, [])
|
}, [])
|
||||||
if (newVariables.length > 0) {
|
if (newVariables.length > 0) {
|
||||||
const newSessionState = await updateVariables(state)(newVariables)
|
newSessionState = await updateVariables(newSessionState)(newVariables)
|
||||||
return {
|
return {
|
||||||
outgoingEdgeId,
|
outgoingEdgeId,
|
||||||
newSessionState,
|
newSessionState,
|
||||||
@@ -81,5 +90,99 @@ export const createChatCompletionOpenAI = async (
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
outgoingEdgeId,
|
outgoingEdgeId,
|
||||||
|
newSessionState,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const log = {
|
||||||
|
status: 'error',
|
||||||
|
description: 'OpenAI block returned error',
|
||||||
|
details: JSON.stringify(err, null, 2).substring(0, 1000),
|
||||||
|
}
|
||||||
|
state.result &&
|
||||||
|
(await saveErrorLog({
|
||||||
|
resultId: state.result.id,
|
||||||
|
message: log.description,
|
||||||
|
details: log.details,
|
||||||
|
}))
|
||||||
|
return {
|
||||||
|
outgoingEdgeId,
|
||||||
|
logs: [log],
|
||||||
|
newSessionState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMessages =
|
||||||
|
(variables: Variable[]) =>
|
||||||
|
(
|
||||||
|
messages: ChatCompletionOpenAIOptions['messages']
|
||||||
|
): {
|
||||||
|
variablesTransformedToList: VariableWithValue[]
|
||||||
|
messages: ChatCompletionRequestMessage[]
|
||||||
|
} => {
|
||||||
|
const variablesTransformedToList: VariableWithValue[] = []
|
||||||
|
const parsedMessages = messages
|
||||||
|
.flatMap((message) => {
|
||||||
|
if (!message.role) return
|
||||||
|
if (message.role === 'Messages sequence ✨') {
|
||||||
|
if (
|
||||||
|
!message.content?.assistantMessagesVariableId ||
|
||||||
|
!message.content?.userMessagesVariableId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
variablesTransformedToList.push(
|
||||||
|
...transformStringVariablesToList(variables)([
|
||||||
|
message.content.assistantMessagesVariableId,
|
||||||
|
message.content.userMessagesVariableId,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
const updatedVariables = variables.map((variable) => {
|
||||||
|
const variableTransformedToList = variablesTransformedToList.find(
|
||||||
|
byId(variable.id)
|
||||||
|
)
|
||||||
|
if (variableTransformedToList) return variableTransformedToList
|
||||||
|
return variable
|
||||||
|
})
|
||||||
|
|
||||||
|
const userMessages = (updatedVariables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === message.content?.userMessagesVariableId
|
||||||
|
)?.value ?? []) as string[]
|
||||||
|
|
||||||
|
const assistantMessages = (updatedVariables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === message.content?.assistantMessagesVariableId
|
||||||
|
)?.value ?? []) as string[]
|
||||||
|
|
||||||
|
if (userMessages.length > assistantMessages.length)
|
||||||
|
return userMessages.flatMap((userMessage, index) => [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: userMessage,
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: assistantMessages[index] },
|
||||||
|
]) satisfies ChatCompletionRequestMessage[]
|
||||||
|
else {
|
||||||
|
return assistantMessages.flatMap((assistantMessage, index) => [
|
||||||
|
{ role: 'assistant', content: assistantMessage },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: userMessages[index],
|
||||||
|
},
|
||||||
|
]) satisfies ChatCompletionRequestMessage[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: message.role,
|
||||||
|
content: parseVariables(variables)(message.content),
|
||||||
|
} satisfies ChatCompletionRequestMessage
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(message) => isNotEmpty(message?.role) && isNotEmpty(message?.content)
|
||||||
|
) as ChatCompletionRequestMessage[]
|
||||||
|
|
||||||
|
return {
|
||||||
|
variablesTransformedToList,
|
||||||
|
messages: parsedMessages,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,12 @@ const saveVariableValueIfAny =
|
|||||||
if (!foundVariable) return state
|
if (!foundVariable) return state
|
||||||
|
|
||||||
const newSessionState = await updateVariables(state)([
|
const newSessionState = await updateVariables(state)([
|
||||||
{ ...foundVariable, value: reply },
|
{
|
||||||
|
...foundVariable,
|
||||||
|
value: Array.isArray(foundVariable.value)
|
||||||
|
? foundVariable.value.concat(reply)
|
||||||
|
: reply,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
return newSessionState
|
return newSessionState
|
||||||
|
|||||||
@@ -124,15 +124,12 @@ const computeRuntimeOptions =
|
|||||||
|
|
||||||
const getPrefilledInputValue =
|
const getPrefilledInputValue =
|
||||||
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
|
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
|
||||||
return (
|
const variableValue = variables.find(
|
||||||
variables
|
|
||||||
.find(
|
|
||||||
(variable) =>
|
(variable) =>
|
||||||
variable.id === block.options.variableId &&
|
variable.id === block.options.variableId && isDefined(variable.value)
|
||||||
isDefined(variable.value)
|
)?.value
|
||||||
)
|
if (!variableValue || Array.isArray(variableValue)) return
|
||||||
?.value?.toString() ?? undefined
|
return variableValue
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBubbleBlock =
|
const parseBubbleBlock =
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Variable, VariableWithValue } from 'models'
|
||||||
|
import { isNotDefined } from 'utils'
|
||||||
|
|
||||||
|
export const transformStringVariablesToList =
|
||||||
|
(variables: Variable[]) =>
|
||||||
|
(variableIds: string[]): VariableWithValue[] => {
|
||||||
|
const newVariables = variables.reduce<VariableWithValue[]>(
|
||||||
|
(variables, variable) => {
|
||||||
|
if (
|
||||||
|
!variableIds.includes(variable.id) ||
|
||||||
|
isNotDefined(variable.value) ||
|
||||||
|
typeof variable.value !== 'string'
|
||||||
|
)
|
||||||
|
return variables
|
||||||
|
return [
|
||||||
|
...variables,
|
||||||
|
{
|
||||||
|
...variable,
|
||||||
|
value: [variable.value],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
return newVariables
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ export const chatCompletionMessageRoles = [
|
|||||||
'assistant',
|
'assistant',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
export const chatCompletionMessageCustomRoles = [
|
||||||
|
'Messages sequence ✨',
|
||||||
|
] as const
|
||||||
|
|
||||||
export const chatCompletionResponseValues = [
|
export const chatCompletionResponseValues = [
|
||||||
'Message content',
|
'Message content',
|
||||||
'Total tokens',
|
'Total tokens',
|
||||||
@@ -36,11 +40,24 @@ const chatCompletionMessageSchema = z.object({
|
|||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const chatCompletionCustomMessageSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
role: z.enum(chatCompletionMessageCustomRoles),
|
||||||
|
content: z
|
||||||
|
.object({
|
||||||
|
assistantMessagesVariableId: z.string().optional(),
|
||||||
|
userMessagesVariableId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
const chatCompletionOptionsSchema = z
|
const chatCompletionOptionsSchema = z
|
||||||
.object({
|
.object({
|
||||||
task: z.literal(openAITasks[0]),
|
task: z.literal(openAITasks[0]),
|
||||||
model: z.enum(chatCompletionModels),
|
model: z.enum(chatCompletionModels),
|
||||||
messages: z.array(chatCompletionMessageSchema),
|
messages: z.array(
|
||||||
|
z.union([chatCompletionMessageSchema, chatCompletionCustomMessageSchema])
|
||||||
|
),
|
||||||
responseMapping: z.array(
|
responseMapping: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user