@@ -56,7 +56,6 @@
|
|||||||
"@uiw/codemirror-theme-tokyo-night": "4.21.24",
|
"@uiw/codemirror-theme-tokyo-night": "4.21.24",
|
||||||
"@uiw/react-codemirror": "4.21.24",
|
"@uiw/react-codemirror": "4.21.24",
|
||||||
"@upstash/ratelimit": "0.4.3",
|
"@upstash/ratelimit": "0.4.3",
|
||||||
"@upstash/redis": "1.22.0",
|
|
||||||
"@use-gesture/react": "10.2.27",
|
"@use-gesture/react": "10.2.27",
|
||||||
"browser-image-compression": "2.0.2",
|
"browser-image-compression": "2.0.2",
|
||||||
"canvas-confetti": "1.6.0",
|
"canvas-confetti": "1.6.0",
|
||||||
@@ -69,6 +68,7 @@
|
|||||||
"google-auth-library": "8.9.0",
|
"google-auth-library": "8.9.0",
|
||||||
"google-spreadsheet": "4.1.1",
|
"google-spreadsheet": "4.1.1",
|
||||||
"immer": "10.0.2",
|
"immer": "10.0.2",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
"isolated-vm": "4.7.2",
|
"isolated-vm": "4.7.2",
|
||||||
"jsonwebtoken": "9.0.1",
|
"jsonwebtoken": "9.0.1",
|
||||||
"ky": "1.2.4",
|
"ky": "1.2.4",
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
Portal,
|
Portal,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React from 'react'
|
import React, { RefObject } from 'react'
|
||||||
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
||||||
import { ImageUploadContent } from './ImageUploadContent'
|
import { ImageUploadContent } from './ImageUploadContent'
|
||||||
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uploadFileProps: FilePathUploadProps
|
uploadFileProps: FilePathUploadProps
|
||||||
icon?: string | null
|
icon?: string | null
|
||||||
|
parentModalRef?: RefObject<HTMLElement | null> | undefined
|
||||||
onChangeIcon: (icon: string) => void
|
onChangeIcon: (icon: string) => void
|
||||||
boxSize?: string
|
boxSize?: string
|
||||||
}
|
}
|
||||||
@@ -28,6 +30,7 @@ export const EditableEmojiOrImageIcon = ({
|
|||||||
boxSize,
|
boxSize,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
|
const { ref: parentModalRef } = useParentModal()
|
||||||
const bg = useColorModeValue('gray.100', 'gray.700')
|
const bg = useColorModeValue('gray.100', 'gray.700')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +59,7 @@ export const EditableEmojiOrImageIcon = ({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Portal>
|
<Portal containerRef={parentModalRef}>
|
||||||
<PopoverContent p="2">
|
<PopoverContent p="2">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
uploadFileProps={uploadFileProps}
|
uploadFileProps={uploadFileProps}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ test.describe('Date input block', () => {
|
|||||||
'date'
|
'date'
|
||||||
)
|
)
|
||||||
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
|
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
|
||||||
await page.locator('form').getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await expect(page.locator('text="01/01/2021"')).toBeVisible()
|
await expect(page.locator('text="01/01/2021"')).toBeVisible()
|
||||||
|
|
||||||
await page.click(`text=Pick a date`)
|
await page.click(`text=Pick a date`)
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ test('options should work', async ({ page }) => {
|
|||||||
await page
|
await page
|
||||||
.locator(`input[type="file"]`)
|
.locator(`input[type="file"]`)
|
||||||
.setInputFiles([getTestAsset('avatar.jpg')])
|
.setInputFiles([getTestAsset('avatar.jpg')])
|
||||||
await expect(page.locator(`text=File uploaded`)).toBeVisible()
|
await expect(
|
||||||
|
page.getByRole('img', { name: 'Attached image 1' })
|
||||||
|
).toBeVisible()
|
||||||
await page.click('text="Collect file"')
|
await page.click('text="Collect file"')
|
||||||
await page.click('text="Required?"')
|
await page.click('text="Required?"')
|
||||||
await page.click('text="Allow multiple files?"')
|
await page.click('text="Allow multiple files?"')
|
||||||
@@ -46,9 +48,11 @@ test('options should work', async ({ page }) => {
|
|||||||
getTestAsset('avatar.jpg'),
|
getTestAsset('avatar.jpg'),
|
||||||
getTestAsset('avatar.jpg'),
|
getTestAsset('avatar.jpg'),
|
||||||
])
|
])
|
||||||
await expect(page.locator(`text="3"`)).toBeVisible()
|
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(3)
|
||||||
await page.locator('text="Go"').click()
|
await page.locator('text="Go"').click()
|
||||||
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
|
await expect(
|
||||||
|
page.getByRole('img', { name: 'Attached image 1' })
|
||||||
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Free workspace', () => {
|
test.describe('Free workspace', () => {
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@chakra-ui/react'
|
import { Stack, Text } from '@chakra-ui/react'
|
||||||
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
|
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
|
||||||
import { TextInputBlock } from '@typebot.io/schemas'
|
import { TextInputBlock } from '@typebot.io/schemas'
|
||||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||||
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: TextInputBlock['options']
|
options: TextInputBlock['options']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInputNodeContent = ({ options }: Props) => {
|
export const TextInputNodeContent = ({ options }: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const attachmentVariableId =
|
||||||
|
typebot &&
|
||||||
|
options?.attachments?.isEnabled &&
|
||||||
|
options?.attachments.saveVariableId
|
||||||
if (options?.variableId)
|
if (options?.variableId)
|
||||||
return (
|
return (
|
||||||
<WithVariableContent
|
<Stack w="calc(100% - 25px)">
|
||||||
variableId={options?.variableId}
|
<WithVariableContent
|
||||||
h={options.isLong ? '100px' : 'auto'}
|
variableId={options?.variableId}
|
||||||
/>
|
h={options.isLong ? '100px' : 'auto'}
|
||||||
|
/>
|
||||||
|
{attachmentVariableId && (
|
||||||
|
<SetVariableLabel
|
||||||
|
variables={typebot.variables}
|
||||||
|
variableId={attachmentVariableId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
|
<Stack>
|
||||||
{options?.labels?.placeholder ??
|
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
|
||||||
defaultTextInputOptions.labels.placeholder}
|
{options?.labels?.placeholder ??
|
||||||
</Text>
|
defaultTextInputOptions.labels.placeholder}
|
||||||
|
</Text>
|
||||||
|
{attachmentVariableId && (
|
||||||
|
<SetVariableLabel
|
||||||
|
variables={typebot.variables}
|
||||||
|
variableId={attachmentVariableId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { DropdownList } from '@/components/DropdownList'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
import { TextInput } from '@/components/inputs'
|
import { TextInput } from '@/components/inputs'
|
||||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { TextInputBlock, Variable } from '@typebot.io/schemas'
|
import { TextInputBlock, Variable } from '@typebot.io/schemas'
|
||||||
|
import { fileVisibilityOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -14,21 +17,44 @@ type Props = {
|
|||||||
|
|
||||||
export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
|
export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const handlePlaceholderChange = (placeholder: string) =>
|
const updatePlaceholder = (placeholder: string) =>
|
||||||
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
|
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
|
||||||
const handleButtonLabelChange = (button: string) =>
|
|
||||||
|
const updateButtonLabel = (button: string) =>
|
||||||
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
|
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
|
||||||
const handleLongChange = (isLong: boolean) =>
|
|
||||||
|
const updateIsLong = (isLong: boolean) =>
|
||||||
onOptionsChange({ ...options, isLong })
|
onOptionsChange({ ...options, isLong })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
|
||||||
|
const updateVariableId = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
|
||||||
|
const updateAttachmentsEnabled = (isEnabled: boolean) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
attachments: { ...options?.attachments, isEnabled },
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAttachmentsSaveVariableId = (variable?: Pick<Variable, 'id'>) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
attachments: { ...options?.attachments, saveVariableId: variable?.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateVisibility = (
|
||||||
|
visibility: (typeof fileVisibilityOptions)[number]
|
||||||
|
) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
attachments: { ...options?.attachments, visibility },
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
label={t('blocks.inputs.text.settings.longText.label')}
|
label={t('blocks.inputs.text.settings.longText.label')}
|
||||||
initialValue={options?.isLong ?? defaultTextInputOptions.isLong}
|
initialValue={options?.isLong ?? defaultTextInputOptions.isLong}
|
||||||
onCheckChange={handleLongChange}
|
onCheckChange={updateIsLong}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('blocks.inputs.settings.placeholder.label')}
|
label={t('blocks.inputs.settings.placeholder.label')}
|
||||||
@@ -36,22 +62,50 @@ export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
options?.labels?.placeholder ??
|
options?.labels?.placeholder ??
|
||||||
defaultTextInputOptions.labels.placeholder
|
defaultTextInputOptions.labels.placeholder
|
||||||
}
|
}
|
||||||
onChange={handlePlaceholderChange}
|
onChange={updatePlaceholder}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('blocks.inputs.settings.button.label')}
|
label={t('blocks.inputs.settings.button.label')}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
options?.labels?.button ?? defaultTextInputOptions.labels.button
|
options?.labels?.button ?? defaultTextInputOptions.labels.button
|
||||||
}
|
}
|
||||||
onChange={handleButtonLabelChange}
|
onChange={updateButtonLabel}
|
||||||
/>
|
/>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label={'Allow attachments'}
|
||||||
|
initialValue={
|
||||||
|
options?.attachments?.isEnabled ??
|
||||||
|
defaultTextInputOptions.attachments.isEnabled
|
||||||
|
}
|
||||||
|
onCheckChange={updateAttachmentsEnabled}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
|
Save the URLs in a variable:
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.attachments?.saveVariableId}
|
||||||
|
onSelectVariable={updateAttachmentsSaveVariableId}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<DropdownList
|
||||||
|
label="Visibility:"
|
||||||
|
moreInfoTooltip='This setting determines who can see the uploaded files. "Public" means that anyone who has the link can see the files. "Private" means that only a members of this workspace can see the files.'
|
||||||
|
currentItem={
|
||||||
|
options?.attachments?.visibility ??
|
||||||
|
defaultTextInputOptions.attachments.visibility
|
||||||
|
}
|
||||||
|
onItemSelect={updateVisibility}
|
||||||
|
items={fileVisibilityOptions}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="variable">
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
{t('blocks.inputs.settings.saveAnswer.label')}
|
{t('blocks.inputs.settings.saveAnswer.label')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<VariableSearchInput
|
<VariableSearchInput
|
||||||
initialVariableId={options?.variableId}
|
initialVariableId={options?.variableId}
|
||||||
onSelectVariable={handleVariableChange}
|
onSelectVariable={updateVariableId}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpe
|
|||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||||
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
|
||||||
test.describe.parallel('Text input block', () => {
|
test.describe.parallel('Text input block', () => {
|
||||||
test('options should work', async ({ page }) => {
|
test('options should work', async ({ page }) => {
|
||||||
@@ -37,4 +38,48 @@ test.describe.parallel('Text input block', () => {
|
|||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('hey boy', async ({ page }) => {
|
||||||
|
const typebotId = createId()
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
...parseDefaultGroupWithBlock({
|
||||||
|
type: InputBlockType.TEXT,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
|
await page.click(`text=${defaultTextInputOptions.labels.placeholder}`)
|
||||||
|
await page.getByText('Allow attachments').click()
|
||||||
|
await page.locator('[data-testid="variables-input"]').first().click()
|
||||||
|
await page.getByText('var1').click()
|
||||||
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Type your answer...')
|
||||||
|
.fill('Help me with these')
|
||||||
|
await page.getByLabel('Add attachments').click()
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Document' })).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Photos & videos' })
|
||||||
|
).toBeVisible()
|
||||||
|
await page
|
||||||
|
.locator('#document-upload')
|
||||||
|
.setInputFiles(getTestAsset('typebots/theme.json'))
|
||||||
|
await expect(page.getByText('theme.json')).toBeVisible()
|
||||||
|
await page
|
||||||
|
.locator('#photos-upload')
|
||||||
|
.setInputFiles([getTestAsset('avatar.jpg'), getTestAsset('avatar.jpg')])
|
||||||
|
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(2)
|
||||||
|
await page.getByRole('img', { name: 'avatar.jpg' }).first().hover()
|
||||||
|
await page.getByLabel('Remove attachment').first().click()
|
||||||
|
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(1)
|
||||||
|
await page.getByLabel('Send').click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('img', { name: 'Attached image 1' })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(page.getByText('Help me with these')).toBeVisible()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
|
|
||||||
await page.click('text=Add a value')
|
await page.click('text=Add a value')
|
||||||
await page.click('text=Select a column')
|
await page.click('text=Select a column')
|
||||||
await page.click('button >> text="Email"')
|
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||||
await page.click('[aria-label="Insert a variable"]')
|
await page.click('[aria-label="Insert a variable"]')
|
||||||
await page.click('button >> text="Email" >> nth=1')
|
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||||
|
|
||||||
await page.click('text=Add a value')
|
await page.click('text=Add a value')
|
||||||
await page.click('text=Select a column')
|
await page.click('text=Select a column')
|
||||||
@@ -61,11 +61,11 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
await page.getByRole('button', { name: 'Row(s) to update' }).click()
|
await page.getByRole('button', { name: 'Row(s) to update' }).click()
|
||||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||||
await page.click('text=Select a column')
|
await page.click('text=Select a column')
|
||||||
await page.click('button >> text="Email"')
|
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||||
await page.getByRole('button', { name: 'Select an operator' }).click()
|
await page.getByRole('button', { name: 'Select an operator' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
||||||
await page.click('[aria-label="Insert a variable"]')
|
await page.click('[aria-label="Insert a variable"]')
|
||||||
await page.click('button >> text="Email" >> nth=1')
|
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cells to update' }).click()
|
await page.getByRole('button', { name: 'Cells to update' }).click()
|
||||||
await page.click('text=Add a value')
|
await page.click('text=Add a value')
|
||||||
@@ -106,11 +106,11 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
await page.getByRole('button', { name: 'Select row(s)' }).click()
|
await page.getByRole('button', { name: 'Select row(s)' }).click()
|
||||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||||
await page.click('text=Select a column')
|
await page.click('text=Select a column')
|
||||||
await page.click('button >> text="Email"')
|
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||||
await page.getByRole('button', { name: 'Select an operator' }).click()
|
await page.getByRole('button', { name: 'Select an operator' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
||||||
await page.click('[aria-label="Insert a variable"]')
|
await page.click('[aria-label="Insert a variable"]')
|
||||||
await page.click('button >> text="Email" >> nth=1')
|
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||||
await page.getByRole('button', { name: 'AND', exact: true }).click()
|
await page.getByRole('button', { name: 'AND', exact: true }).click()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ test.describe('Condition block', () => {
|
|||||||
'input[placeholder="Search for a variable"] >> nth=-1',
|
'input[placeholder="Search for a variable"] >> nth=-1',
|
||||||
'Age'
|
'Age'
|
||||||
)
|
)
|
||||||
await page.click('button:has-text("Age")')
|
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||||
await page.click('button:has-text("Select an operator")')
|
await page.click('button:has-text("Select an operator")')
|
||||||
await page.click('button:has-text("Greater than")', { force: true })
|
await page.click('button:has-text("Greater than")', { force: true })
|
||||||
await page.fill('input[placeholder="Type a number..."]', '80')
|
await page.fill('input[placeholder="Type a number..."]', '80')
|
||||||
@@ -31,7 +31,7 @@ test.describe('Condition block', () => {
|
|||||||
':nth-match(input[placeholder="Search for a variable"], 2)',
|
':nth-match(input[placeholder="Search for a variable"], 2)',
|
||||||
'Age'
|
'Age'
|
||||||
)
|
)
|
||||||
await page.click('button:has-text("Age")')
|
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||||
await page.click('button:has-text("Select an operator")')
|
await page.click('button:has-text("Select an operator")')
|
||||||
await page.click('button:has-text("Less than")', { force: true })
|
await page.click('button:has-text("Less than")', { force: true })
|
||||||
await page.fill(
|
await page.fill(
|
||||||
@@ -44,7 +44,7 @@ test.describe('Condition block', () => {
|
|||||||
'input[placeholder="Search for a variable"] >> nth=-1',
|
'input[placeholder="Search for a variable"] >> nth=-1',
|
||||||
'Age'
|
'Age'
|
||||||
)
|
)
|
||||||
await page.click('button:has-text("Age")')
|
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||||
await page.click('button:has-text("Select an operator")')
|
await page.click('button:has-text("Select an operator")')
|
||||||
await page.click('button:has-text("Greater than")', { force: true })
|
await page.click('button:has-text("Greater than")', { force: true })
|
||||||
await page.fill('input[placeholder="Type a number..."]', '20')
|
await page.fill('input[placeholder="Type a number..."]', '20')
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ test.describe('Set variable block', () => {
|
|||||||
await page.click('text=Click to edit... >> nth = 0')
|
await page.click('text=Click to edit... >> nth = 0')
|
||||||
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
|
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
|
||||||
await page.getByRole('menuitem', { name: 'Create Total' }).click()
|
await page.getByRole('menuitem', { name: 'Create Total' }).click()
|
||||||
await page
|
await page.locator('textarea').fill('1000 * {{Num}}')
|
||||||
.getByTestId('code-editor')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('1000 * {{Num}}')
|
|
||||||
|
|
||||||
await page.click('text=Click to edit...', { force: true })
|
await page.click('text=Click to edit...', { force: true })
|
||||||
await expect(page.getByText('Save in results?')).toBeHidden()
|
await expect(page.getByText('Save in results?')).toBeHidden()
|
||||||
@@ -39,10 +36,7 @@ test.describe('Set variable block', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('group').nth(1).locator('.chakra-switch')
|
page.getByRole('group').nth(1).locator('.chakra-switch')
|
||||||
).not.toHaveAttribute('data-checked')
|
).not.toHaveAttribute('data-checked')
|
||||||
await page
|
await page.locator('textarea').fill('Custom value')
|
||||||
.getByTestId('code-editor')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('Custom value')
|
|
||||||
|
|
||||||
await page.click('text=Click to edit...', { force: true })
|
await page.click('text=Click to edit...', { force: true })
|
||||||
await page.fill(
|
await page.fill(
|
||||||
@@ -50,10 +44,7 @@ test.describe('Set variable block', () => {
|
|||||||
'Addition'
|
'Addition'
|
||||||
)
|
)
|
||||||
await page.getByRole('menuitem', { name: 'Create Addition' }).click()
|
await page.getByRole('menuitem', { name: 'Create Addition' }).click()
|
||||||
await page
|
await page.locator('textarea').fill('1000 + {{Total}}')
|
||||||
.getByTestId('code-editor')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('1000 + {{Total}}')
|
|
||||||
|
|
||||||
await page.click('text=Test')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
@@ -94,14 +85,14 @@ test.describe('Set variable block', () => {
|
|||||||
await page.getByRole('button', { name: 'Test' }).click()
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
|
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
|
||||||
await page.getByTestId('textarea').fill('Hello!!')
|
await page.getByTestId('textarea').fill('Hello!!')
|
||||||
await page.getByTestId('input').getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.getByRole('button', { name: 'Restart' })
|
.getByRole('button', { name: 'Restart' })
|
||||||
.click()
|
.click()
|
||||||
await page.getByRole('button', { name: 'I have a question 💭' }).click()
|
await page.getByRole('button', { name: 'I have a question 💭' }).click()
|
||||||
await page.getByTestId('textarea').fill('How are you?')
|
await page.getByTestId('textarea').fill('How are you?')
|
||||||
await page.getByTestId('input').getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await page.getByRole('button', { name: 'Transcription' }).click()
|
await page.getByRole('button', { name: 'Transcription' }).click()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ test('should be configurable', async ({ page }) => {
|
|||||||
await page.getByLabel('Clear').click()
|
await page.getByLabel('Clear').click()
|
||||||
|
|
||||||
await page.click('text=Test')
|
await page.click('text=Test')
|
||||||
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||||
await page.locator('typebot-standard').locator('input').press('Enter')
|
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('typebot-standard').locator('text=Hello there!')
|
page.locator('typebot-standard').locator('text=Hello there!')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTranslate } from '@tolgee/react'
|
|||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown'
|
import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown'
|
||||||
import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
|
import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
|
||||||
|
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
|
||||||
|
|
||||||
export const DashboardHeader = () => {
|
export const DashboardHeader = () => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
@@ -38,12 +39,14 @@ export const DashboardHeader = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<HStack>
|
<HStack>
|
||||||
{user && workspace && !workspace.isPastDue && (
|
{user && workspace && !workspace.isPastDue && (
|
||||||
<WorkspaceSettingsModal
|
<ParentModalProvider>
|
||||||
isOpen={isOpen}
|
<WorkspaceSettingsModal
|
||||||
onClose={onClose}
|
isOpen={isOpen}
|
||||||
user={user}
|
onClose={onClose}
|
||||||
workspace={workspace}
|
user={user}
|
||||||
/>
|
workspace={workspace}
|
||||||
|
/>
|
||||||
|
</ParentModalProvider>
|
||||||
)}
|
)}
|
||||||
{!workspace?.isPastDue && (
|
{!workspace?.isPastDue && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -171,15 +171,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
|
|||||||
const newBlock = { ...block }
|
const newBlock = { ...block }
|
||||||
const blockId = createId()
|
const blockId = createId()
|
||||||
oldToNewIdsMapping.set(newBlock.id, blockId)
|
oldToNewIdsMapping.set(newBlock.id, blockId)
|
||||||
console.log(JSON.stringify(newBlock), blockHasOptions(newBlock))
|
|
||||||
if (blockHasOptions(newBlock) && newBlock.options) {
|
if (blockHasOptions(newBlock) && newBlock.options) {
|
||||||
const variableIdsToReplace = extractVariableIdsFromObject(
|
const variableIdsToReplace = extractVariableIdsFromObject(
|
||||||
newBlock.options
|
newBlock.options
|
||||||
).filter((v) => oldToNewIdsMapping.has(v))
|
).filter((v) => oldToNewIdsMapping.has(v))
|
||||||
console.log(
|
|
||||||
JSON.stringify(newBlock.options),
|
|
||||||
variableIdsToReplace
|
|
||||||
)
|
|
||||||
if (variableIdsToReplace.length > 0) {
|
if (variableIdsToReplace.length > 0) {
|
||||||
let optionsStr = JSON.stringify(newBlock.options)
|
let optionsStr = JSON.stringify(newBlock.options)
|
||||||
variableIdsToReplace.forEach((variableId) => {
|
variableIdsToReplace.forEach((variableId) => {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const typebotImportCode = isCloudProdInstance()
|
export const typebotImportCode = isCloudProdInstance()
|
||||||
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'`
|
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'`
|
||||||
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
|
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
|
||||||
|
|
||||||
export const parseInlineScript = (script: string) =>
|
export const parseInlineScript = (script: string) =>
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export const startWhatsAppPreview = authenticatedProcedure
|
|||||||
setVariableHistory,
|
setVariableHistory,
|
||||||
} = await startSession({
|
} = await startSession({
|
||||||
version: 2,
|
version: 2,
|
||||||
message: undefined,
|
|
||||||
startParams: {
|
startParams: {
|
||||||
isOnlyRegistering: !canSendDirectMessagesToUser,
|
isOnlyRegistering: !canSendDirectMessagesToUser,
|
||||||
type: 'preview',
|
type: 'preview',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { UserPreferencesForm } from '@/features/account/components/UserPreferenc
|
|||||||
import { MyAccountForm } from '@/features/account/components/MyAccountForm'
|
import { MyAccountForm } from '@/features/account/components/MyAccountForm'
|
||||||
import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout'
|
import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -47,6 +48,7 @@ export const WorkspaceSettingsModal = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
|
const { ref } = useParentModal()
|
||||||
const { currentRole } = useWorkspace()
|
const { currentRole } = useWorkspace()
|
||||||
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ export const WorkspaceSettingsModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent minH="600px" flexDir="row">
|
<ModalContent minH="600px" flexDir="row" ref={ref}>
|
||||||
<Stack
|
<Stack
|
||||||
spacing={8}
|
spacing={8}
|
||||||
w="180px"
|
w="180px"
|
||||||
|
|||||||
@@ -16,20 +16,36 @@ import { mockedUser } from '@typebot.io/lib/mockedUser'
|
|||||||
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
||||||
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
|
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
|
||||||
import { Ratelimit } from '@upstash/ratelimit'
|
import { Ratelimit } from '@upstash/ratelimit'
|
||||||
import { Redis } from '@upstash/redis/nodejs'
|
|
||||||
import ky from 'ky'
|
import ky from 'ky'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import * as Sentry from '@sentry/nextjs'
|
import * as Sentry from '@sentry/nextjs'
|
||||||
import { getIp } from '@typebot.io/lib/getIp'
|
import { getIp } from '@typebot.io/lib/getIp'
|
||||||
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
|
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
|
||||||
|
import Redis from 'ioredis'
|
||||||
|
|
||||||
const providers: Provider[] = []
|
const providers: Provider[] = []
|
||||||
|
|
||||||
let rateLimit: Ratelimit | undefined
|
let emailSignInRateLimiter: Ratelimit | undefined
|
||||||
|
|
||||||
if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) {
|
if (env.REDIS_URL) {
|
||||||
rateLimit = new Ratelimit({
|
const redis = new Redis(env.REDIS_URL)
|
||||||
redis: Redis.fromEnv(),
|
const rateLimitCompatibleRedis = {
|
||||||
|
sadd: <TData>(key: string, ...members: TData[]) =>
|
||||||
|
redis.sadd(key, ...members.map((m) => String(m))),
|
||||||
|
eval: async <TArgs extends unknown[], TData = unknown>(
|
||||||
|
script: string,
|
||||||
|
keys: string[],
|
||||||
|
args: TArgs
|
||||||
|
) =>
|
||||||
|
redis.eval(
|
||||||
|
script,
|
||||||
|
keys.length,
|
||||||
|
...keys,
|
||||||
|
...(args ?? []).map((a) => String(a))
|
||||||
|
) as Promise<TData>,
|
||||||
|
}
|
||||||
|
emailSignInRateLimiter = new Ratelimit({
|
||||||
|
redis: rateLimitCompatibleRedis,
|
||||||
limiter: Ratelimit.slidingWindow(1, '60 s'),
|
limiter: Ratelimit.slidingWindow(1, '60 s'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -229,13 +245,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
let restricted: 'rate-limited' | undefined
|
let restricted: 'rate-limited' | undefined
|
||||||
|
|
||||||
if (
|
if (
|
||||||
rateLimit &&
|
emailSignInRateLimiter &&
|
||||||
req.url?.startsWith('/api/auth/signin/email') &&
|
req.url?.startsWith('/api/auth/signin/email') &&
|
||||||
req.method === 'POST'
|
req.method === 'POST'
|
||||||
) {
|
) {
|
||||||
const ip = getIp(req)
|
const ip = getIp(req)
|
||||||
if (ip) {
|
if (ip) {
|
||||||
const { success } = await rateLimit.limit(ip)
|
const { success } = await emailSignInRateLimiter.limit(ip)
|
||||||
if (!success) restricted = 'rate-limited'
|
if (!success) restricted = 'rate-limited'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
|
import {
|
||||||
|
methodNotAllowed,
|
||||||
|
notAuthenticated,
|
||||||
|
notFound,
|
||||||
|
} from '@typebot.io/lib/api'
|
||||||
|
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||||
|
import { downloadMedia } from '@typebot.io/bot-engine/whatsapp/downloadMedia'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
if (!env.META_SYSTEM_USER_TOKEN)
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'Meta system user token is not set' })
|
||||||
|
const user = await getAuthenticatedUser(req, res)
|
||||||
|
if (!user) return notAuthenticated(res)
|
||||||
|
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
|
|
||||||
|
const typebot = await prisma.typebot.findFirst({
|
||||||
|
where: {
|
||||||
|
id: typebotId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
whatsAppCredentialsId: true,
|
||||||
|
workspace: {
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!typebot?.workspace || isReadWorkspaceFobidden(typebot.workspace, user))
|
||||||
|
return notFound(res, 'Workspace not found')
|
||||||
|
|
||||||
|
if (!typebot) return notFound(res, 'Typebot not found')
|
||||||
|
|
||||||
|
const mediaId = req.query.mediaId as string
|
||||||
|
|
||||||
|
const { file, mimeType } = await downloadMedia({
|
||||||
|
mediaId,
|
||||||
|
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', mimeType)
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||||
|
|
||||||
|
return res.send(file)
|
||||||
|
}
|
||||||
|
return methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
||||||
@@ -10,7 +10,7 @@ There, you can change the container dimensions. Here is a code example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initStandard({
|
Typebot.initStandard({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
@@ -28,7 +28,7 @@ If you have different bots on the same page you will have to make them distinct
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initStandard({
|
Typebot.initStandard({
|
||||||
id: 'bot1'
|
id: 'bot1'
|
||||||
@@ -60,7 +60,7 @@ Here is an example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initPopup({
|
Typebot.initPopup({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
@@ -80,7 +80,7 @@ Here is an example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initBubble({
|
Typebot.initBubble({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ It should look like:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initPopup({
|
Typebot.initPopup({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
|
|||||||
@@ -11157,6 +11157,12 @@
|
|||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"attachedFileUrls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -11452,6 +11458,12 @@
|
|||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"attachedFileUrls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -14664,6 +14676,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -14698,6 +14713,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -14766,6 +14784,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -15605,6 +15626,25 @@
|
|||||||
},
|
},
|
||||||
"isLong": {
|
"isLong": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"saveVariableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Auto",
|
||||||
|
"Public",
|
||||||
|
"Private"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,14 +174,12 @@ The Authorization callback URL should be `$NEXTAUTH_URL/api/auth/callback/azure-
|
|||||||
Used for authenticating with Keycloak.
|
Used for authenticating with Keycloak.
|
||||||
Follow the official Keycloak guide for creating OAuth2 applications [here](https://www.keycloak.org/).
|
Follow the official Keycloak guide for creating OAuth2 applications [here](https://www.keycloak.org/).
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
| ------------------------ | ------------------ | ------------------------------------------------------------------------------------ |
|
| ---------------------- | ------- | --------------------------------- |
|
||||||
| KEYCLOAK_CLIENT_ID | | Application client ID. |
|
| KEYCLOAK_CLIENT_ID | | Application client ID. |
|
||||||
| KEYCLOAK_CLIENT_SECRET | | Application secret |
|
| KEYCLOAK_CLIENT_SECRET | | Application secret |
|
||||||
| KEYCLOAK_REALM | | Your Keycloak Realm |
|
| KEYCLOAK_REALM | | Your Keycloak Realm |
|
||||||
| KEYCLOAK_BASE_URL | | Base URL of the Keycloak instance |
|
| KEYCLOAK_BASE_URL | | Base URL of the Keycloak instance |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Custom OAuth Provider (Auth)
|
## Custom OAuth Provider (Auth)
|
||||||
|
|
||||||
@@ -306,6 +304,17 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne
|
|||||||
| WHATSAPP_CLOUD_API_URL | https://graph.facebook.com | The WhatsApp Cloud API base URL |
|
| WHATSAPP_CLOUD_API_URL | https://graph.facebook.com | The WhatsApp Cloud API base URL |
|
||||||
| WHATSAPP_INTERACTIVE_GROUP_SIZE | 3 | The array size of items to send to API on choice input. You can't choose a number higher than 3 if you are using the official cloud API URL. |
|
| WHATSAPP_INTERACTIVE_GROUP_SIZE | 3 | The array size of items to send to API on choice input. You can't choose a number higher than 3 if you are using the official cloud API URL. |
|
||||||
|
|
||||||
|
## Redis
|
||||||
|
|
||||||
|
In Typebot, Redis is optional and is used to:
|
||||||
|
|
||||||
|
- Rate limit the sign in requests based on user IP
|
||||||
|
- Enable multiple media upload on WhatsApp
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| --------- | ------- | -------------------------------------------------------------------- |
|
||||||
|
| REDIS_URL | | The database URL. i.e. `redis://<username>:<password>@<host>:<port>` |
|
||||||
|
|
||||||
## Others
|
## Others
|
||||||
|
|
||||||
The [official Typebot managed service](https://app.typebot.io/) uses other services such as [Stripe](https://stripe.com/) for processing payments, [Sentry](https://sentry.io/) for tracking bugs and [Sleekplan](https://sleekplan.com/) for user feedbacks.
|
The [official Typebot managed service](https://app.typebot.io/) uses other services such as [Stripe](https://stripe.com/) for processing payments, [Sentry](https://sentry.io/) for tracking bugs and [Sleekplan](https://sleekplan.com/) for user feedbacks.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { publicProcedure } from '@/helpers/server/trpc'
|
import { publicProcedure } from '@/helpers/server/trpc'
|
||||||
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
|
import {
|
||||||
|
continueChatResponseSchema,
|
||||||
|
messageSchema,
|
||||||
|
} from '@typebot.io/schemas/features/chat/schema'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
|
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
|
||||||
|
|
||||||
@@ -13,7 +16,7 @@ export const continueChat = publicProcedure
|
|||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
message: z.string().optional(),
|
message: messageSchema.optional(),
|
||||||
sessionId: z
|
sessionId: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ export const sendMessageV1 = publicProcedure
|
|||||||
typeof startParams.typebot === 'string'
|
typeof startParams.typebot === 'string'
|
||||||
? undefined
|
? undefined
|
||||||
: startParams.typebot,
|
: startParams.typebot,
|
||||||
message,
|
message: message
|
||||||
|
? { type: 'text', text: message }
|
||||||
|
: undefined,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
textBubbleContentFormat: 'richText',
|
textBubbleContentFormat: 'richText',
|
||||||
}
|
}
|
||||||
@@ -102,10 +104,11 @@ export const sendMessageV1 = publicProcedure
|
|||||||
publicId: startParams.typebot,
|
publicId: startParams.typebot,
|
||||||
prefilledVariables: startParams.prefilledVariables,
|
prefilledVariables: startParams.prefilledVariables,
|
||||||
resultId: startParams.resultId,
|
resultId: startParams.resultId,
|
||||||
message,
|
message: message
|
||||||
|
? { type: 'text', text: message }
|
||||||
|
: undefined,
|
||||||
textBubbleContentFormat: 'richText',
|
textBubbleContentFormat: 'richText',
|
||||||
},
|
},
|
||||||
message,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
||||||
@@ -185,11 +188,14 @@ export const sendMessageV1 = publicProcedure
|
|||||||
lastMessageNewFormat,
|
lastMessageNewFormat,
|
||||||
visitedEdges,
|
visitedEdges,
|
||||||
setVariableHistory,
|
setVariableHistory,
|
||||||
} = await continueBotFlow(message, {
|
} = await continueBotFlow(
|
||||||
version: 1,
|
message ? { type: 'text', text: message } : undefined,
|
||||||
state: session.state,
|
{
|
||||||
textBubbleContentFormat: 'richText',
|
version: 1,
|
||||||
})
|
state: session.state,
|
||||||
|
textBubbleContentFormat: 'richText',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ export const sendMessageV2 = publicProcedure
|
|||||||
typeof startParams.typebot === 'string'
|
typeof startParams.typebot === 'string'
|
||||||
? undefined
|
? undefined
|
||||||
: startParams.typebot,
|
: startParams.typebot,
|
||||||
message,
|
message: message
|
||||||
|
? { type: 'text', text: message }
|
||||||
|
: undefined,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
textBubbleContentFormat: 'richText',
|
textBubbleContentFormat: 'richText',
|
||||||
}
|
}
|
||||||
@@ -102,10 +104,11 @@ export const sendMessageV2 = publicProcedure
|
|||||||
publicId: startParams.typebot,
|
publicId: startParams.typebot,
|
||||||
prefilledVariables: startParams.prefilledVariables,
|
prefilledVariables: startParams.prefilledVariables,
|
||||||
resultId: startParams.resultId,
|
resultId: startParams.resultId,
|
||||||
message,
|
message: message
|
||||||
|
? { type: 'text', text: message }
|
||||||
|
: undefined,
|
||||||
textBubbleContentFormat: 'richText',
|
textBubbleContentFormat: 'richText',
|
||||||
},
|
},
|
||||||
message,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
||||||
@@ -184,11 +187,14 @@ export const sendMessageV2 = publicProcedure
|
|||||||
lastMessageNewFormat,
|
lastMessageNewFormat,
|
||||||
visitedEdges,
|
visitedEdges,
|
||||||
setVariableHistory,
|
setVariableHistory,
|
||||||
} = await continueBotFlow(message, {
|
} = await continueBotFlow(
|
||||||
version: 2,
|
message ? { type: 'text', text: message } : undefined,
|
||||||
state: session.state,
|
{
|
||||||
textBubbleContentFormat: 'richText',
|
version: 2,
|
||||||
})
|
state: session.state,
|
||||||
|
textBubbleContentFormat: 'richText',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresigne
|
|||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||||
import { parseGroups } from '@typebot.io/schemas'
|
import {
|
||||||
|
FileInputBlock,
|
||||||
|
parseGroups,
|
||||||
|
TextInputBlock,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { getBlockById } from '@typebot.io/schemas/helpers'
|
import { getBlockById } from '@typebot.io/schemas/helpers'
|
||||||
|
import { PublicTypebot } from '@typebot.io/prisma'
|
||||||
|
|
||||||
export const generateUploadUrl = publicProcedure
|
export const generateUploadUrl = publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -50,27 +55,18 @@ export const generateUploadUrl = publicProcedure
|
|||||||
|
|
||||||
const typebotId = session.state.typebotsQueue[0].typebot.id
|
const typebotId = session.state.typebotsQueue[0].typebot.id
|
||||||
|
|
||||||
const publicTypebot = await prisma.publicTypebot.findFirst({
|
const isPreview = session.state.typebotsQueue[0].resultId
|
||||||
where: {
|
|
||||||
typebotId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
version: true,
|
|
||||||
groups: true,
|
|
||||||
typebot: {
|
|
||||||
select: {
|
|
||||||
workspaceId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const workspaceId = publicTypebot?.typebot.workspaceId
|
const typebot = session.state.typebotsQueue[0].resultId
|
||||||
|
? await getAndParsePublicTypebot(
|
||||||
|
session.state.typebotsQueue[0].typebot.id
|
||||||
|
)
|
||||||
|
: session.state.typebotsQueue[0].typebot
|
||||||
|
|
||||||
if (!workspaceId)
|
if (!typebot?.version)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message: "Can't find workspaceId",
|
message: "Can't find typebot",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (session.state.currentBlockId === undefined)
|
if (session.state.currentBlockId === undefined)
|
||||||
@@ -79,42 +75,93 @@ export const generateUploadUrl = publicProcedure
|
|||||||
message: "Can't find currentBlockId in session state",
|
message: "Can't find currentBlockId in session state",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { block: fileUploadBlock } = getBlockById(
|
const { block } = getBlockById(
|
||||||
session.state.currentBlockId,
|
session.state.currentBlockId,
|
||||||
parseGroups(publicTypebot.groups, {
|
parseGroups(typebot.groups, {
|
||||||
typebotVersion: publicTypebot.version,
|
typebotVersion: typebot.version,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (fileUploadBlock?.type !== InputBlockType.FILE)
|
if (
|
||||||
|
block?.type !== InputBlockType.FILE &&
|
||||||
|
(block.type !== InputBlockType.TEXT ||
|
||||||
|
!block.options?.attachments?.isEnabled)
|
||||||
|
)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message: "Can't find file upload block",
|
message: 'Current block does not expect file upload',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { visibility, maxFileSize } = parseFileUploadParams(block)
|
||||||
|
|
||||||
const resultId = session.state.typebotsQueue[0].resultId
|
const resultId = session.state.typebotsQueue[0].resultId
|
||||||
|
|
||||||
const filePath = `${
|
const filePath =
|
||||||
fileUploadBlock.options?.visibility === 'Private' ? 'private' : 'public'
|
'workspaceId' in typebot && typebot.workspaceId
|
||||||
}/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${fileName}`
|
? `${visibility === 'Private' ? 'private' : 'public'}/workspaces/${
|
||||||
|
typebot.workspaceId
|
||||||
|
}/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||||
|
: `/public/tmp/${typebotId}/${fileName}`
|
||||||
|
|
||||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||||
fileType,
|
fileType,
|
||||||
filePath,
|
filePath,
|
||||||
maxFileSize:
|
maxFileSize,
|
||||||
fileUploadBlock.options && 'sizeLimit' in fileUploadBlock.options
|
|
||||||
? (fileUploadBlock.options.sizeLimit as number)
|
|
||||||
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presignedUrl: presignedPostPolicy.postURL,
|
presignedUrl: presignedPostPolicy.postURL,
|
||||||
formData: presignedPostPolicy.formData,
|
formData: presignedPostPolicy.formData,
|
||||||
fileUrl:
|
fileUrl:
|
||||||
fileUploadBlock.options?.visibility === 'Private'
|
visibility === 'Private' && !isPreview
|
||||||
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${fileName}`
|
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||||
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getAndParsePublicTypebot = async (typebotId: string) => {
|
||||||
|
const publicTypebot = (await prisma.publicTypebot.findFirst({
|
||||||
|
where: {
|
||||||
|
typebotId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
version: true,
|
||||||
|
groups: true,
|
||||||
|
typebot: {
|
||||||
|
select: {
|
||||||
|
workspaceId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})) as (PublicTypebot & { typebot: { workspaceId: string } }) | null
|
||||||
|
|
||||||
|
return {
|
||||||
|
...publicTypebot,
|
||||||
|
workspaceId: publicTypebot?.typebot.workspaceId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseFileUploadParams = (
|
||||||
|
block: FileInputBlock | TextInputBlock
|
||||||
|
): { visibility: 'Public' | 'Private'; maxFileSize: number | undefined } => {
|
||||||
|
if (block.type === InputBlockType.FILE) {
|
||||||
|
return {
|
||||||
|
visibility:
|
||||||
|
block.options?.visibility === 'Private' ? 'Private' : 'Public',
|
||||||
|
maxFileSize:
|
||||||
|
block.options && 'sizeLimit' in block.options
|
||||||
|
? (block.options.sizeLimit as number)
|
||||||
|
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibility:
|
||||||
|
block.options?.attachments?.visibility === 'Private'
|
||||||
|
? 'Private'
|
||||||
|
: 'Public',
|
||||||
|
maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
|
|||||||
|
|
||||||
test.afterEach(async () => {
|
test.afterEach(async () => {
|
||||||
await deleteWebhooks(['chat-webhook-id'])
|
await deleteWebhooks(['chat-webhook-id'])
|
||||||
await deleteTypebots(['chat-sub-bot'])
|
await deleteTypebots(['chat-sub-bot', 'starting-with-input'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('API chat execution should work on preview bot', async ({ request }) => {
|
test('API chat execution should work on preview bot', async ({ request }) => {
|
||||||
@@ -108,6 +108,13 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
|||||||
id: 'chat-sub-bot',
|
id: 'chat-sub-bot',
|
||||||
publicId: 'chat-sub-bot-public',
|
publicId: 'chat-sub-bot-public',
|
||||||
})
|
})
|
||||||
|
await importTypebotInDatabase(
|
||||||
|
getTestAsset('typebots/chat/startingWithInput.json'),
|
||||||
|
{
|
||||||
|
id: 'starting-with-input',
|
||||||
|
publicId: 'starting-with-input-public',
|
||||||
|
}
|
||||||
|
)
|
||||||
await createWebhook(typebotId, {
|
await createWebhook(typebotId, {
|
||||||
id: 'chat-webhook-id',
|
id: 'chat-webhook-id',
|
||||||
method: HttpMethod.GET,
|
method: HttpMethod.GET,
|
||||||
@@ -296,11 +303,12 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
|||||||
expect(messages[2].content.richText.length).toBeGreaterThan(0)
|
expect(messages[2].content.richText.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
|
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
|
||||||
const { messages } = await (
|
const response = await (
|
||||||
await request.post(
|
await request.post(
|
||||||
`/api/v1/typebots/starting-with-input-public/startChat`,
|
`/api/v1/typebots/starting-with-input-public/startChat`,
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
//@ts-expect-error We want to test if message is correctly preprocessed by zod
|
||||||
message: 'Hey',
|
message: 'Hey',
|
||||||
isStreamEnabled: false,
|
isStreamEnabled: false,
|
||||||
isOnlyRegistering: false,
|
isOnlyRegistering: false,
|
||||||
@@ -309,7 +317,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
).json()
|
).json()
|
||||||
expect(messages[0].content.richText).toStrictEqual([
|
expect(response.messages[0].content.richText).toStrictEqual([
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ test('Transcript set variable should be correctly computed', async ({
|
|||||||
|
|
||||||
await page.goto(`/${typebotId}-public`)
|
await page.goto(`/${typebotId}-public`)
|
||||||
await page.getByPlaceholder('Type your answer...').fill('hey')
|
await page.getByPlaceholder('Type your answer...').fill('hey')
|
||||||
await page.getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await page.getByPlaceholder('Type your answer...').fill('hey 2')
|
await page.getByPlaceholder('Type your answer...').fill('hey 2')
|
||||||
await page.getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await page.getByPlaceholder('Type your answer...').fill('hey 3')
|
await page.getByPlaceholder('Type your answer...').fill('hey 3')
|
||||||
await page.getByRole('button').click()
|
await page.getByLabel('Send').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Assistant: "How are you? You said "')
|
page.getByText('Assistant: "How are you? You said "')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ test.beforeAll(async () => {
|
|||||||
|
|
||||||
test('should work as expected', async ({ page }) => {
|
test('should work as expected', async ({ page }) => {
|
||||||
await page.goto(`/${typebotId}-public`)
|
await page.goto(`/${typebotId}-public`)
|
||||||
await page.locator('input').fill('Hello there!')
|
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||||
await page.locator('input').press('Enter')
|
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||||
await expect(page.getByText('Cheers!')).toBeVisible()
|
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||||
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||||
await expect(page.locator('text=Hello there!')).toBeVisible()
|
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||||
@@ -41,8 +41,8 @@ test('should work as expected', async ({ page }) => {
|
|||||||
test.describe('Merge disabled', () => {
|
test.describe('Merge disabled', () => {
|
||||||
test('should work as expected', async ({ page }) => {
|
test('should work as expected', async ({ page }) => {
|
||||||
await page.goto(`/${typebotWithMergeDisabledId}-public`)
|
await page.goto(`/${typebotWithMergeDisabledId}-public`)
|
||||||
await page.locator('input').fill('Hello there!')
|
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||||
await page.locator('input').press('Enter')
|
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||||
await expect(page.getByText('Cheers!')).toBeVisible()
|
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||||
await page.goto(
|
await page.goto(
|
||||||
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`
|
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { parseDynamicTheme } from '../parseDynamicTheme'
|
|||||||
import { saveStateToDatabase } from '../saveStateToDatabase'
|
import { saveStateToDatabase } from '../saveStateToDatabase'
|
||||||
import { computeCurrentProgress } from '../computeCurrentProgress'
|
import { computeCurrentProgress } from '../computeCurrentProgress'
|
||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
|
import { Message } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
origin: string | undefined
|
origin: string | undefined
|
||||||
message?: string
|
message?: Message
|
||||||
sessionId: string
|
sessionId: string
|
||||||
textBubbleContentFormat: 'richText' | 'markdown'
|
textBubbleContentFormat: 'richText' | 'markdown'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
|
import { Message } from '@typebot.io/schemas'
|
||||||
import { computeCurrentProgress } from '../computeCurrentProgress'
|
import { computeCurrentProgress } from '../computeCurrentProgress'
|
||||||
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
|
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
|
||||||
import { restartSession } from '../queries/restartSession'
|
import { restartSession } from '../queries/restartSession'
|
||||||
import { saveStateToDatabase } from '../saveStateToDatabase'
|
import { saveStateToDatabase } from '../saveStateToDatabase'
|
||||||
import { startSession } from '../startSession'
|
import { startSession } from '../startSession'
|
||||||
import { isNotEmpty } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
origin: string | undefined
|
origin: string | undefined
|
||||||
message?: string
|
message?: Message
|
||||||
isOnlyRegistering: boolean
|
isOnlyRegistering: boolean
|
||||||
publicId: string
|
publicId: string
|
||||||
isStreamEnabled: boolean
|
isStreamEnabled: boolean
|
||||||
@@ -48,8 +48,8 @@ export const startChat = async ({
|
|||||||
prefilledVariables,
|
prefilledVariables,
|
||||||
resultId: startResultId,
|
resultId: startResultId,
|
||||||
textBubbleContentFormat,
|
textBubbleContentFormat,
|
||||||
|
message,
|
||||||
},
|
},
|
||||||
message,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let corsOrigin
|
let corsOrigin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { StartFrom, StartTypebot } from '@typebot.io/schemas'
|
import { Message, StartFrom, StartTypebot } from '@typebot.io/schemas'
|
||||||
import { restartSession } from '../queries/restartSession'
|
import { restartSession } from '../queries/restartSession'
|
||||||
import { saveStateToDatabase } from '../saveStateToDatabase'
|
import { saveStateToDatabase } from '../saveStateToDatabase'
|
||||||
import { startSession } from '../startSession'
|
import { startSession } from '../startSession'
|
||||||
@@ -6,7 +6,7 @@ import { computeCurrentProgress } from '../computeCurrentProgress'
|
|||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message?: string
|
message?: Message
|
||||||
isOnlyRegistering: boolean
|
isOnlyRegistering: boolean
|
||||||
isStreamEnabled: boolean
|
isStreamEnabled: boolean
|
||||||
startFrom?: StartFrom
|
startFrom?: StartFrom
|
||||||
@@ -53,8 +53,8 @@ export const startChatPreview = async ({
|
|||||||
prefilledVariables,
|
prefilledVariables,
|
||||||
sessionId,
|
sessionId,
|
||||||
textBubbleContentFormat,
|
textBubbleContentFormat,
|
||||||
|
message,
|
||||||
},
|
},
|
||||||
message,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const session = isOnlyRegistering
|
const session = isOnlyRegistering
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||||
import { createCodeRunner } from '@typebot.io/variables/codeRunners'
|
import { createCodeRunner } from '@typebot.io/variables/codeRunners'
|
||||||
import { stringifyError } from '@typebot.io/lib/stringifyError'
|
import { stringifyError } from '@typebot.io/lib/stringifyError'
|
||||||
|
import { AnswerV2 } from '@typebot.io/prisma'
|
||||||
|
|
||||||
export const executeSetVariable = async (
|
export const executeSetVariable = async (
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
@@ -246,7 +247,7 @@ const toISOWithTz = (date: Date, timeZone: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ParsedTranscriptProps = {
|
type ParsedTranscriptProps = {
|
||||||
answers: Pick<Answer, 'blockId' | 'content'>[]
|
answers: Pick<Answer, 'blockId' | 'content' | 'attachedFileUrls'>[]
|
||||||
setVariableHistory: Pick<
|
setVariableHistory: Pick<
|
||||||
SetVariableHistoryItem,
|
SetVariableHistoryItem,
|
||||||
'blockId' | 'variableId' | 'value'
|
'blockId' | 'variableId' | 'value'
|
||||||
@@ -273,6 +274,10 @@ const parsePreviewTranscriptProps = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnifiedAnswersFromDB = (ParsedTranscriptProps['answers'][number] & {
|
||||||
|
createdAt: Date
|
||||||
|
})[]
|
||||||
|
|
||||||
const parseResultTranscriptProps = async (
|
const parseResultTranscriptProps = async (
|
||||||
state: SessionState
|
state: SessionState
|
||||||
): Promise<ParsedTranscriptProps | undefined> => {
|
): Promise<ParsedTranscriptProps | undefined> => {
|
||||||
@@ -299,6 +304,7 @@ const parseResultTranscriptProps = async (
|
|||||||
blockId: true,
|
blockId: true,
|
||||||
content: true,
|
content: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
attachedFileUrls: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setVariableHistory: {
|
setVariableHistory: {
|
||||||
@@ -313,8 +319,8 @@ const parseResultTranscriptProps = async (
|
|||||||
})
|
})
|
||||||
if (!result) return
|
if (!result) return
|
||||||
return {
|
return {
|
||||||
answers: result.answersV2
|
answers: (result.answersV2 as UnifiedAnswersFromDB)
|
||||||
.concat(result.answers)
|
.concat(result.answers as UnifiedAnswersFromDB)
|
||||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()),
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()),
|
||||||
setVariableHistory: (
|
setVariableHistory: (
|
||||||
result.setVariableHistory as SetVariableHistoryItem[]
|
result.setVariableHistory as SetVariableHistoryItem[]
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
ContinueChatResponse,
|
ContinueChatResponse,
|
||||||
Group,
|
Group,
|
||||||
InputBlock,
|
InputBlock,
|
||||||
|
Message,
|
||||||
SessionState,
|
SessionState,
|
||||||
SetVariableHistoryItem,
|
SetVariableHistoryItem,
|
||||||
Variable,
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { byId } from '@typebot.io/lib'
|
import { byId, isDefined } from '@typebot.io/lib'
|
||||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||||
import { executeGroup, parseInput } from './executeGroup'
|
import { executeGroup, parseInput } from './executeGroup'
|
||||||
import { getNextGroup } from './getNextGroup'
|
import { getNextGroup } from './getNextGroup'
|
||||||
@@ -42,10 +43,9 @@ import { ForgedBlock } from '@typebot.io/forge-repository/types'
|
|||||||
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
||||||
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
|
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import { downloadMedia } from './whatsapp/downloadMedia'
|
|
||||||
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
|
||||||
import { isURL } from '@typebot.io/lib/validators/isURL'
|
import { isURL } from '@typebot.io/lib/validators/isURL'
|
||||||
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
|
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
|
||||||
|
import { resetSessionState } from './resetSessionState'
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
version: 1 | 2
|
version: 1 | 2
|
||||||
@@ -69,7 +69,11 @@ export const continueBotFlow = async (
|
|||||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||||
|
|
||||||
if (!newSessionState.currentBlockId)
|
if (!newSessionState.currentBlockId)
|
||||||
return startBotFlow({ state, version, textBubbleContentFormat })
|
return startBotFlow({
|
||||||
|
state: resetSessionState(newSessionState),
|
||||||
|
version,
|
||||||
|
textBubbleContentFormat,
|
||||||
|
})
|
||||||
|
|
||||||
const { block, group, blockIndex } = getBlockById(
|
const { block, group, blockIndex } = getBlockById(
|
||||||
newSessionState.currentBlockId,
|
newSessionState.currentBlockId,
|
||||||
@@ -161,7 +165,7 @@ export const continueBotFlow = async (
|
|||||||
newVariables: [
|
newVariables: [
|
||||||
{
|
{
|
||||||
...variableToUpdate,
|
...variableToUpdate,
|
||||||
value: safeJsonParse(reply as string),
|
value: reply?.text ? safeJsonParse(reply?.text) : undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -187,7 +191,14 @@ export const continueBotFlow = async (
|
|||||||
|
|
||||||
formattedReply =
|
formattedReply =
|
||||||
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
|
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
|
||||||
newSessionState = await processAndSaveAnswer(state, block)(formattedReply)
|
newSessionState = await processAndSaveAnswer(
|
||||||
|
state,
|
||||||
|
block
|
||||||
|
)(
|
||||||
|
isDefined(formattedReply)
|
||||||
|
? { ...reply, type: 'text', text: formattedReply }
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
||||||
@@ -267,37 +278,92 @@ export const continueBotFlow = async (
|
|||||||
|
|
||||||
const processAndSaveAnswer =
|
const processAndSaveAnswer =
|
||||||
(state: SessionState, block: InputBlock) =>
|
(state: SessionState, block: InputBlock) =>
|
||||||
async (reply: string | undefined): Promise<SessionState> => {
|
async (reply: Message | undefined): Promise<SessionState> => {
|
||||||
if (!reply) return state
|
if (!reply) return state
|
||||||
let newState = await saveAnswerInDb(state, block)(reply)
|
return saveAnswerInDb(state, block)(reply)
|
||||||
return newState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveVariableValueIfAny =
|
const saveVariablesValueIfAny =
|
||||||
(state: SessionState, block: InputBlock) =>
|
(state: SessionState, block: InputBlock) =>
|
||||||
(reply: string): SessionState => {
|
(reply: Message): SessionState => {
|
||||||
if (!block.options?.variableId) return state
|
if (!block.options?.variableId) return state
|
||||||
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
const newSessionState = saveAttachmentsVarIfAny({ block, reply, state })
|
||||||
(variable) => variable.id === block.options?.variableId
|
return saveInputVarIfAny({ block, reply, state: newSessionState })
|
||||||
)
|
|
||||||
if (!foundVariable) return state
|
|
||||||
|
|
||||||
const { updatedState } = updateVariablesInSession({
|
|
||||||
newVariables: [
|
|
||||||
{
|
|
||||||
...foundVariable,
|
|
||||||
value: Array.isArray(foundVariable.value)
|
|
||||||
? foundVariable.value.concat(reply)
|
|
||||||
: reply,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
currentBlockId: undefined,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
|
|
||||||
return updatedState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveAttachmentsVarIfAny = ({
|
||||||
|
block,
|
||||||
|
reply,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
block: InputBlock
|
||||||
|
reply: Message
|
||||||
|
state: SessionState
|
||||||
|
}): SessionState => {
|
||||||
|
if (
|
||||||
|
block.type !== InputBlockType.TEXT ||
|
||||||
|
!block.options?.attachments?.isEnabled ||
|
||||||
|
!block.options?.attachments?.saveVariableId ||
|
||||||
|
!reply.attachedFileUrls ||
|
||||||
|
reply.attachedFileUrls.length === 0
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
const variable = state.typebotsQueue[0].typebot.variables.find(
|
||||||
|
(variable) => variable.id === block.options?.attachments?.saveVariableId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!variable) return state
|
||||||
|
|
||||||
|
const { updatedState } = updateVariablesInSession({
|
||||||
|
newVariables: [
|
||||||
|
{
|
||||||
|
id: variable.id,
|
||||||
|
name: variable.name,
|
||||||
|
value: Array.isArray(variable.value)
|
||||||
|
? variable.value.concat(reply.attachedFileUrls)
|
||||||
|
: reply.attachedFileUrls.length === 1
|
||||||
|
? reply.attachedFileUrls[0]
|
||||||
|
: reply.attachedFileUrls,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentBlockId: undefined,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
return updatedState
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveInputVarIfAny = ({
|
||||||
|
block,
|
||||||
|
reply,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
block: InputBlock
|
||||||
|
reply: Message
|
||||||
|
state: SessionState
|
||||||
|
}): SessionState => {
|
||||||
|
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||||
|
(variable) => variable.id === block.options?.variableId
|
||||||
|
)
|
||||||
|
if (!foundVariable) return state
|
||||||
|
|
||||||
|
const { updatedState } = updateVariablesInSession({
|
||||||
|
newVariables: [
|
||||||
|
{
|
||||||
|
...foundVariable,
|
||||||
|
value:
|
||||||
|
Array.isArray(foundVariable.value) && reply.text
|
||||||
|
? foundVariable.value.concat(reply.text)
|
||||||
|
: reply.text,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentBlockId: undefined,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedState
|
||||||
|
}
|
||||||
|
|
||||||
const parseRetryMessage =
|
const parseRetryMessage =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
async (
|
async (
|
||||||
@@ -346,26 +412,27 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
|
|||||||
|
|
||||||
const saveAnswerInDb =
|
const saveAnswerInDb =
|
||||||
(state: SessionState, block: InputBlock) =>
|
(state: SessionState, block: InputBlock) =>
|
||||||
async (reply: string): Promise<SessionState> => {
|
async (reply: Message): Promise<SessionState> => {
|
||||||
let newSessionState = state
|
let newSessionState = state
|
||||||
await saveAnswer({
|
await saveAnswer({
|
||||||
answer: {
|
answer: {
|
||||||
blockId: block.id,
|
blockId: block.id,
|
||||||
content: reply,
|
content: reply.text,
|
||||||
|
attachedFileUrls: reply.attachedFileUrls,
|
||||||
},
|
},
|
||||||
reply,
|
|
||||||
state,
|
state,
|
||||||
})
|
})
|
||||||
|
|
||||||
newSessionState = {
|
newSessionState = {
|
||||||
...saveVariableValueIfAny(newSessionState, block)(reply),
|
...saveVariablesValueIfAny(newSessionState, block)(reply),
|
||||||
previewMetadata: state.typebotsQueue[0].resultId
|
previewMetadata: state.typebotsQueue[0].resultId
|
||||||
? newSessionState.previewMetadata
|
? newSessionState.previewMetadata
|
||||||
: {
|
: {
|
||||||
...newSessionState.previewMetadata,
|
...newSessionState.previewMetadata,
|
||||||
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
|
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
|
||||||
blockId: block.id,
|
blockId: block.id,
|
||||||
content: reply,
|
content: reply.text,
|
||||||
|
attachedFileUrls: reply.attachedFileUrls,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -378,7 +445,10 @@ const saveAnswerInDb =
|
|||||||
|
|
||||||
return setNewAnswerInState(newSessionState)({
|
return setNewAnswerInState(newSessionState)({
|
||||||
key: key ?? block.id,
|
key: key ?? block.id,
|
||||||
value: reply,
|
value:
|
||||||
|
(reply.attachedFileUrls ?? []).length > 0
|
||||||
|
? `${reply.attachedFileUrls!.join(', ')}\n\n${reply.text}`
|
||||||
|
: reply.text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,41 +535,17 @@ const getOutgoingEdgeId =
|
|||||||
const parseReply =
|
const parseReply =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
async (reply: Reply, block: InputBlock): Promise<ParsedReply> => {
|
async (reply: Reply, block: InputBlock): Promise<ParsedReply> => {
|
||||||
if (reply && typeof reply !== 'string') {
|
|
||||||
if (block.type !== InputBlockType.FILE) return { status: 'fail' }
|
|
||||||
if (block.options?.visibility !== 'Public') {
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
reply:
|
|
||||||
env.NEXTAUTH_URL +
|
|
||||||
`/api/typebots/${state.typebotsQueue[0].typebot.id}/whatsapp/media/${reply.mediaId}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { file, mimeType } = await downloadMedia({
|
|
||||||
mediaId: reply.mediaId,
|
|
||||||
systemUserAccessToken: reply.accessToken,
|
|
||||||
})
|
|
||||||
const url = await uploadFileToBucket({
|
|
||||||
file,
|
|
||||||
key: `public/workspaces/${reply.workspaceId}/typebots/${state.typebotsQueue[0].typebot.id}/results/${state.typebotsQueue[0].resultId}/${reply.mediaId}`,
|
|
||||||
mimeType,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
reply: url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case InputBlockType.EMAIL: {
|
case InputBlockType.EMAIL: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
const formattedEmail = formatEmail(reply)
|
const formattedEmail = formatEmail(reply.text)
|
||||||
if (!formattedEmail) return { status: 'fail' }
|
if (!formattedEmail) return { status: 'fail' }
|
||||||
return { status: 'success', reply: formattedEmail }
|
return { status: 'success', reply: formattedEmail }
|
||||||
}
|
}
|
||||||
case InputBlockType.PHONE: {
|
case InputBlockType.PHONE: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
const formattedPhone = formatPhoneNumber(
|
const formattedPhone = formatPhoneNumber(
|
||||||
reply,
|
reply.text,
|
||||||
block.options?.defaultCountryCode
|
block.options?.defaultCountryCode
|
||||||
)
|
)
|
||||||
if (!formattedPhone) return { status: 'fail' }
|
if (!formattedPhone) return { status: 'fail' }
|
||||||
@@ -507,58 +553,60 @@ const parseReply =
|
|||||||
}
|
}
|
||||||
case InputBlockType.URL: {
|
case InputBlockType.URL: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
const isValid = isURL(reply, { require_protocol: false })
|
const isValid = isURL(reply.text, { require_protocol: false })
|
||||||
if (!isValid) return { status: 'fail' }
|
if (!isValid) return { status: 'fail' }
|
||||||
return { status: 'success', reply: reply }
|
return { status: 'success', reply: reply.text }
|
||||||
}
|
}
|
||||||
case InputBlockType.CHOICE: {
|
case InputBlockType.CHOICE: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
return parseButtonsReply(state)(reply, block)
|
return parseButtonsReply(state)(reply.text, block)
|
||||||
}
|
}
|
||||||
case InputBlockType.NUMBER: {
|
case InputBlockType.NUMBER: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
const isValid = validateNumber(reply, {
|
const isValid = validateNumber(reply.text, {
|
||||||
options: block.options,
|
options: block.options,
|
||||||
variables: state.typebotsQueue[0].typebot.variables,
|
variables: state.typebotsQueue[0].typebot.variables,
|
||||||
})
|
})
|
||||||
if (!isValid) return { status: 'fail' }
|
if (!isValid) return { status: 'fail' }
|
||||||
return { status: 'success', reply: parseNumber(reply) }
|
return { status: 'success', reply: parseNumber(reply.text) }
|
||||||
}
|
}
|
||||||
case InputBlockType.DATE: {
|
case InputBlockType.DATE: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
return parseDateReply(reply, block)
|
return parseDateReply(reply.text, block)
|
||||||
}
|
}
|
||||||
case InputBlockType.FILE: {
|
case InputBlockType.FILE: {
|
||||||
if (!reply)
|
if (!reply)
|
||||||
return block.options?.isRequired ?? defaultFileInputOptions.isRequired
|
return block.options?.isRequired ?? defaultFileInputOptions.isRequired
|
||||||
? { status: 'fail' }
|
? { status: 'fail' }
|
||||||
: { status: 'skip' }
|
: { status: 'skip' }
|
||||||
const urls = reply.split(', ')
|
const urls = reply.text.split(', ')
|
||||||
const status = urls.some((url) =>
|
const status = urls.some((url) =>
|
||||||
isURL(url, { require_tld: env.S3_ENDPOINT !== 'localhost' })
|
isURL(url, { require_tld: env.S3_ENDPOINT !== 'localhost' })
|
||||||
)
|
)
|
||||||
? 'success'
|
? 'success'
|
||||||
: 'fail'
|
: 'fail'
|
||||||
return { status, reply: reply }
|
if (!block.options?.isMultipleAllowed && urls.length > 1)
|
||||||
|
return { status, reply: reply.text.split(',')[0] }
|
||||||
|
return { status, reply: reply.text }
|
||||||
}
|
}
|
||||||
case InputBlockType.PAYMENT: {
|
case InputBlockType.PAYMENT: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
if (reply === 'fail') return { status: 'fail' }
|
if (reply.text === 'fail') return { status: 'fail' }
|
||||||
return { status: 'success', reply: reply }
|
return { status: 'success', reply: reply.text }
|
||||||
}
|
}
|
||||||
case InputBlockType.RATING: {
|
case InputBlockType.RATING: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
const isValid = validateRatingReply(reply, block)
|
const isValid = validateRatingReply(reply.text, block)
|
||||||
if (!isValid) return { status: 'fail' }
|
if (!isValid) return { status: 'fail' }
|
||||||
return { status: 'success', reply: reply }
|
return { status: 'success', reply: reply.text }
|
||||||
}
|
}
|
||||||
case InputBlockType.PICTURE_CHOICE: {
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
return parsePictureChoicesReply(state)(reply, block)
|
return parsePictureChoicesReply(state)(reply.text, block)
|
||||||
}
|
}
|
||||||
case InputBlockType.TEXT: {
|
case InputBlockType.TEXT: {
|
||||||
if (!reply) return { status: 'fail' }
|
if (!reply) return { status: 'fail' }
|
||||||
return { status: 'success', reply: reply }
|
return { status: 'success', reply: reply.text }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { SessionState } from '@typebot.io/schemas'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
answer: Omit<Prisma.AnswerV2CreateManyInput, 'resultId'>
|
answer: Omit<Prisma.AnswerV2CreateManyInput, 'resultId'>
|
||||||
reply: string
|
|
||||||
state: SessionState
|
state: SessionState
|
||||||
}
|
}
|
||||||
export const saveAnswer = async ({ answer, state }: Props) => {
|
export const saveAnswer = async ({ answer, state }: Props) => {
|
||||||
|
|||||||
20
packages/bot-engine/resetSessionState.ts
Normal file
20
packages/bot-engine/resetSessionState.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||||
|
|
||||||
|
export const resetSessionState = (state: SessionState): SessionState => ({
|
||||||
|
...state,
|
||||||
|
currentSetVariableHistoryIndex: undefined,
|
||||||
|
currentVisitedEdgeIndex: undefined,
|
||||||
|
previewMetadata: undefined,
|
||||||
|
progressMetadata: undefined,
|
||||||
|
typebotsQueue: state.typebotsQueue.map((queueItem) => ({
|
||||||
|
...queueItem,
|
||||||
|
answers: [],
|
||||||
|
typebot: {
|
||||||
|
...queueItem.typebot,
|
||||||
|
variables: queueItem.typebot.variables.map((variable) => ({
|
||||||
|
...variable,
|
||||||
|
value: undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
})
|
||||||
@@ -61,14 +61,12 @@ type StartParams =
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
version: 1 | 2
|
version: 1 | 2
|
||||||
message: Reply
|
|
||||||
startParams: StartParams
|
startParams: StartParams
|
||||||
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
|
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startSession = async ({
|
export const startSession = async ({
|
||||||
version,
|
version,
|
||||||
message,
|
|
||||||
startParams,
|
startParams,
|
||||||
initialSessionState,
|
initialSessionState,
|
||||||
}: Props): Promise<
|
}: Props): Promise<
|
||||||
@@ -188,7 +186,7 @@ export const startSession = async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// If params has message and first block is an input block, we can directly continue the bot flow
|
// If params has message and first block is an input block, we can directly continue the bot flow
|
||||||
if (message) {
|
if (startParams.message) {
|
||||||
const firstEdgeId = getFirstEdgeId({
|
const firstEdgeId = getFirstEdgeId({
|
||||||
typebot: chatReply.newSessionState.typebotsQueue[0].typebot,
|
typebot: chatReply.newSessionState.typebotsQueue[0].typebot,
|
||||||
startEventId:
|
startEventId:
|
||||||
@@ -213,7 +211,7 @@ export const startSession = async ({
|
|||||||
resultId,
|
resultId,
|
||||||
typebot: newSessionState.typebotsQueue[0].typebot,
|
typebot: newSessionState.typebotsQueue[0].typebot,
|
||||||
})
|
})
|
||||||
chatReply = await continueBotFlow(message, {
|
chatReply = await continueBotFlow(startParams.message, {
|
||||||
version,
|
version,
|
||||||
state: {
|
state: {
|
||||||
...newSessionState,
|
...newSessionState,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ContinueChatResponse,
|
ContinueChatResponse,
|
||||||
CustomEmbedBubble,
|
CustomEmbedBubble,
|
||||||
|
Message,
|
||||||
SessionState,
|
SessionState,
|
||||||
SetVariableHistoryItem,
|
SetVariableHistoryItem,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
@@ -21,14 +22,7 @@ export type ExecuteIntegrationResponse = {
|
|||||||
newSetVariableHistory?: SetVariableHistoryItem[]
|
newSetVariableHistory?: SetVariableHistoryItem[]
|
||||||
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
|
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
|
||||||
|
|
||||||
type WhatsAppMediaMessage = {
|
export type Reply = Message | undefined
|
||||||
type: 'whatsapp media'
|
|
||||||
mediaId: string
|
|
||||||
workspaceId?: string
|
|
||||||
accessToken: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Reply = string | WhatsAppMediaMessage | undefined
|
|
||||||
|
|
||||||
export type ParsedReply =
|
export type ParsedReply =
|
||||||
| { status: 'success'; reply: string }
|
| { status: 'success'; reply: string }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SessionState } from '@typebot.io/schemas'
|
import { Block, SessionState } from '@typebot.io/schemas'
|
||||||
import {
|
import {
|
||||||
WhatsAppCredentials,
|
WhatsAppCredentials,
|
||||||
WhatsAppIncomingMessage,
|
WhatsAppIncomingMessage,
|
||||||
@@ -15,6 +15,13 @@ import { isDefined } from '@typebot.io/lib/utils'
|
|||||||
import { Reply } from '../types'
|
import { Reply } from '../types'
|
||||||
import { setIsReplyingInChatSession } from '../queries/setIsReplyingInChatSession'
|
import { setIsReplyingInChatSession } from '../queries/setIsReplyingInChatSession'
|
||||||
import { removeIsReplyingInChatSession } from '../queries/removeIsReplyingInChatSession'
|
import { removeIsReplyingInChatSession } from '../queries/removeIsReplyingInChatSession'
|
||||||
|
import redis from '@typebot.io/lib/redis'
|
||||||
|
import { downloadMedia } from './downloadMedia'
|
||||||
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
|
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
||||||
|
import { getBlockById } from '@typebot.io/schemas/helpers'
|
||||||
|
|
||||||
|
const incomingMessageDebounce = 3000
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
receivedMessage: WhatsAppIncomingMessage
|
receivedMessage: WhatsAppIncomingMessage
|
||||||
@@ -61,33 +68,59 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = await getIncomingMessageContent({
|
|
||||||
message: receivedMessage,
|
|
||||||
workspaceId,
|
|
||||||
accessToken: credentials?.systemUserAccessToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
const session = await getSession(sessionId)
|
const session = await getSession(sessionId)
|
||||||
|
|
||||||
|
const { incomingMessages, isReplyingWasSet } =
|
||||||
|
await aggregateParallelMediaMessagesIfRedisEnabled({
|
||||||
|
receivedMessage,
|
||||||
|
existingSessionId: session?.id,
|
||||||
|
newSessionId: sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (incomingMessages.length === 0) {
|
||||||
|
if (isReplyingWasSet) await removeIsReplyingInChatSession(sessionId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Message received',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isSessionExpired =
|
const isSessionExpired =
|
||||||
session &&
|
session &&
|
||||||
isDefined(session.state.expiryTimeout) &&
|
isDefined(session.state.expiryTimeout) &&
|
||||||
session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
|
session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
|
||||||
|
|
||||||
if (session?.isReplying) {
|
if (!isReplyingWasSet) {
|
||||||
if (!isSessionExpired) {
|
if (session?.isReplying) {
|
||||||
console.log('Is currently replying, skipping...')
|
if (!isSessionExpired) {
|
||||||
return {
|
console.log('Is currently replying, skipping...')
|
||||||
message: 'Message received',
|
return {
|
||||||
|
message: 'Message received',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await setIsReplyingInChatSession({
|
||||||
|
existingSessionId: session?.id,
|
||||||
|
newSessionId: sessionId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
await setIsReplyingInChatSession({
|
|
||||||
existingSessionId: session?.id,
|
|
||||||
newSessionId: sessionId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentTypebot = session?.state.typebotsQueue[0].typebot
|
||||||
|
const { block } =
|
||||||
|
(currentTypebot && session?.state.currentBlockId
|
||||||
|
? getBlockById(session.state.currentBlockId, currentTypebot.groups)
|
||||||
|
: undefined) ?? {}
|
||||||
|
|
||||||
|
const reply = await getIncomingMessageContent({
|
||||||
|
messages: incomingMessages,
|
||||||
|
workspaceId,
|
||||||
|
accessToken: credentials?.systemUserAccessToken,
|
||||||
|
typebotId: currentTypebot?.id,
|
||||||
|
resultId: session?.state.typebotsQueue[0].resultId,
|
||||||
|
block,
|
||||||
|
})
|
||||||
|
|
||||||
const resumeResponse =
|
const resumeResponse =
|
||||||
session && !isSessionExpired
|
session && !isSessionExpired
|
||||||
? await continueBotFlow(reply, {
|
? await continueBotFlow(reply, {
|
||||||
@@ -155,35 +188,107 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getIncomingMessageContent = async ({
|
const getIncomingMessageContent = async ({
|
||||||
message,
|
messages,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
typebotId,
|
||||||
|
resultId,
|
||||||
|
block,
|
||||||
}: {
|
}: {
|
||||||
message: WhatsAppIncomingMessage
|
messages: WhatsAppIncomingMessage[]
|
||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
accessToken: string
|
accessToken: string
|
||||||
|
typebotId?: string
|
||||||
|
resultId?: string
|
||||||
|
block?: Block
|
||||||
}): Promise<Reply> => {
|
}): Promise<Reply> => {
|
||||||
switch (message.type) {
|
let text: string = ''
|
||||||
case 'text':
|
const attachedFileUrls: string[] = []
|
||||||
return message.text.body
|
for (const message of messages) {
|
||||||
case 'button':
|
switch (message.type) {
|
||||||
return message.button.text
|
case 'text': {
|
||||||
case 'interactive': {
|
if (text !== '') text += `\n\n${message.text.body}`
|
||||||
return message.interactive.button_reply.id
|
else text = message.text.body
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'button': {
|
||||||
|
if (text !== '') text += `\n\n${message.button.text}`
|
||||||
|
else text = message.button.text
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'interactive': {
|
||||||
|
if (text !== '') text += `\n\n${message.interactive.button_reply.id}`
|
||||||
|
else text = message.interactive.button_reply.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'document':
|
||||||
|
case 'audio':
|
||||||
|
case 'video':
|
||||||
|
case 'image': {
|
||||||
|
let mediaId: string | undefined
|
||||||
|
if (message.type === 'video') mediaId = message.video.id
|
||||||
|
if (message.type === 'image') mediaId = message.image.id
|
||||||
|
if (message.type === 'audio') mediaId = message.audio.id
|
||||||
|
if (message.type === 'document') mediaId = message.document.id
|
||||||
|
if (!mediaId) return
|
||||||
|
const fileVisibility =
|
||||||
|
block?.type === InputBlockType.FILE
|
||||||
|
? block.options?.visibility
|
||||||
|
: block?.type === InputBlockType.TEXT
|
||||||
|
? block.options?.attachments?.visibility
|
||||||
|
: undefined
|
||||||
|
let fileUrl
|
||||||
|
if (fileVisibility !== 'Public') {
|
||||||
|
fileUrl =
|
||||||
|
env.NEXTAUTH_URL +
|
||||||
|
`/api/typebots/${typebotId}/whatsapp/media/${
|
||||||
|
workspaceId ? `` : 'preview/'
|
||||||
|
}${mediaId}`
|
||||||
|
} else {
|
||||||
|
const { file, mimeType } = await downloadMedia({
|
||||||
|
mediaId,
|
||||||
|
systemUserAccessToken: accessToken,
|
||||||
|
})
|
||||||
|
const url = await uploadFileToBucket({
|
||||||
|
file,
|
||||||
|
key:
|
||||||
|
resultId && workspaceId && typebotId
|
||||||
|
? `public/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${mediaId}`
|
||||||
|
: `tmp/whatsapp/media/${mediaId}`,
|
||||||
|
mimeType,
|
||||||
|
})
|
||||||
|
fileUrl = url
|
||||||
|
}
|
||||||
|
if (block?.type === InputBlockType.FILE) {
|
||||||
|
if (text !== '') text += `, ${fileUrl}`
|
||||||
|
else text = fileUrl
|
||||||
|
} else if (block?.type === InputBlockType.TEXT) {
|
||||||
|
let caption: string | undefined
|
||||||
|
if (message.type === 'document' && message.document.caption) {
|
||||||
|
if (!/^[\w,\s-]+\.[A-Za-z]{3}$/.test(message.document.caption))
|
||||||
|
caption = message.document.caption
|
||||||
|
} else if (message.type === 'image' && message.image.caption)
|
||||||
|
caption = message.image.caption
|
||||||
|
else if (message.type === 'video' && message.video.caption)
|
||||||
|
caption = message.video.caption
|
||||||
|
if (caption) text = text === '' ? caption : `${text}\n\n${caption}`
|
||||||
|
attachedFileUrls.push(fileUrl)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'location': {
|
||||||
|
const location = `${message.location.latitude}, ${message.location.longitude}`
|
||||||
|
if (text !== '') text += `\n\n${location}`
|
||||||
|
else text = location
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'document':
|
}
|
||||||
case 'audio':
|
|
||||||
case 'video':
|
return {
|
||||||
case 'image':
|
type: 'text',
|
||||||
let mediaId: string | undefined
|
text,
|
||||||
if (message.type === 'video') mediaId = message.video.id
|
attachedFileUrls,
|
||||||
if (message.type === 'image') mediaId = message.image.id
|
|
||||||
if (message.type === 'audio') mediaId = message.audio.id
|
|
||||||
if (message.type === 'document') mediaId = message.document.id
|
|
||||||
if (!mediaId) return
|
|
||||||
return { type: 'whatsapp media', mediaId, workspaceId, accessToken }
|
|
||||||
case 'location':
|
|
||||||
return `${message.location.latitude}, ${message.location.longitude}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,3 +332,57 @@ const getCredentials = async ({
|
|||||||
phoneNumberId: data.phoneNumberId,
|
phoneNumberId: data.phoneNumberId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const aggregateParallelMediaMessagesIfRedisEnabled = async ({
|
||||||
|
receivedMessage,
|
||||||
|
existingSessionId,
|
||||||
|
newSessionId,
|
||||||
|
}: {
|
||||||
|
receivedMessage: WhatsAppIncomingMessage
|
||||||
|
existingSessionId?: string
|
||||||
|
newSessionId: string
|
||||||
|
}): Promise<{
|
||||||
|
isReplyingWasSet: boolean
|
||||||
|
incomingMessages: WhatsAppIncomingMessage[]
|
||||||
|
}> => {
|
||||||
|
if (redis && ['document', 'video', 'image'].includes(receivedMessage.type)) {
|
||||||
|
const redisKey = `wasession:${newSessionId}`
|
||||||
|
try {
|
||||||
|
const len = await redis.rpush(redisKey, JSON.stringify(receivedMessage))
|
||||||
|
|
||||||
|
if (len === 1) {
|
||||||
|
await setIsReplyingInChatSession({
|
||||||
|
existingSessionId,
|
||||||
|
newSessionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, incomingMessageDebounce)
|
||||||
|
)
|
||||||
|
|
||||||
|
const newMessagesResponse = await redis.lrange(redisKey, 0, -1)
|
||||||
|
|
||||||
|
if (!newMessagesResponse || newMessagesResponse.length > len) {
|
||||||
|
// Current message was aggregated with other messages another webhook handler. Skipping...
|
||||||
|
return { isReplyingWasSet: true, incomingMessages: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.del(redisKey).then()
|
||||||
|
|
||||||
|
return {
|
||||||
|
isReplyingWasSet: true,
|
||||||
|
incomingMessages: newMessagesResponse.map((msgStr) =>
|
||||||
|
JSON.parse(msgStr)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to process webhook event:', error, receivedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isReplyingWasSet: false,
|
||||||
|
incomingMessages: [receivedMessage],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,11 +58,16 @@ export const sendChatReplyToWhatsApp = async ({
|
|||||||
const result = await executeClientSideAction({ to, credentials })(action)
|
const result = await executeClientSideAction({ to, credentials })(action)
|
||||||
if (!result) continue
|
if (!result) continue
|
||||||
const { input, newSessionState, messages, clientSideActions } =
|
const { input, newSessionState, messages, clientSideActions } =
|
||||||
await continueBotFlow(result.replyToSend, {
|
await continueBotFlow(
|
||||||
version: 2,
|
result.replyToSend
|
||||||
state,
|
? { type: 'text', text: result.replyToSend }
|
||||||
textBubbleContentFormat: 'richText',
|
: undefined,
|
||||||
})
|
{
|
||||||
|
version: 2,
|
||||||
|
state,
|
||||||
|
textBubbleContentFormat: 'richText',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return sendChatReplyToWhatsApp({
|
return sendChatReplyToWhatsApp({
|
||||||
to,
|
to,
|
||||||
@@ -128,11 +133,16 @@ export const sendChatReplyToWhatsApp = async ({
|
|||||||
)
|
)
|
||||||
if (!result) continue
|
if (!result) continue
|
||||||
const { input, newSessionState, messages, clientSideActions } =
|
const { input, newSessionState, messages, clientSideActions } =
|
||||||
await continueBotFlow(result.replyToSend, {
|
await continueBotFlow(
|
||||||
version: 2,
|
result.replyToSend
|
||||||
state,
|
? { type: 'text', text: result.replyToSend }
|
||||||
textBubbleContentFormat: 'richText',
|
: undefined,
|
||||||
})
|
{
|
||||||
|
version: 2,
|
||||||
|
state,
|
||||||
|
textBubbleContentFormat: 'richText',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return sendChatReplyToWhatsApp({
|
return sendChatReplyToWhatsApp({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const startWhatsAppSession = async ({
|
|||||||
(publicTypebot.settings.whatsApp?.startCondition?.comparisons.length ??
|
(publicTypebot.settings.whatsApp?.startCondition?.comparisons.length ??
|
||||||
0) > 0 &&
|
0) > 0 &&
|
||||||
messageMatchStartCondition(
|
messageMatchStartCondition(
|
||||||
incomingMessage ?? '',
|
incomingMessage ?? { type: 'text', text: '' },
|
||||||
publicTypebot.settings.whatsApp?.startCondition
|
publicTypebot.settings.whatsApp?.startCondition
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -90,13 +90,13 @@ export const startWhatsAppSession = async ({
|
|||||||
|
|
||||||
return startSession({
|
return startSession({
|
||||||
version: 2,
|
version: 2,
|
||||||
message: incomingMessage,
|
|
||||||
startParams: {
|
startParams: {
|
||||||
type: 'live',
|
type: 'live',
|
||||||
publicId: publicTypebot.typebot.publicId as string,
|
publicId: publicTypebot.typebot.publicId as string,
|
||||||
isOnlyRegistering: false,
|
isOnlyRegistering: false,
|
||||||
isStreamEnabled: false,
|
isStreamEnabled: false,
|
||||||
textBubbleContentFormat: 'richText',
|
textBubbleContentFormat: 'richText',
|
||||||
|
message: incomingMessage,
|
||||||
},
|
},
|
||||||
initialSessionState: {
|
initialSessionState: {
|
||||||
whatsApp: {
|
whatsApp: {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ npm install @typebot.io/js
|
|||||||
|
|
||||||
```
|
```
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initStandard({
|
Typebot.initStandard({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
@@ -34,7 +34,7 @@ There, you can change the container dimensions. Here is a code example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initStandard({
|
Typebot.initStandard({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
@@ -54,7 +54,7 @@ Here is an example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initPopup({
|
Typebot.initPopup({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
@@ -96,7 +96,7 @@ Here is an example:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||||
|
|
||||||
Typebot.initBubble({
|
Typebot.initBubble({
|
||||||
typebot: 'my-typebot',
|
typebot: 'my-typebot',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.2.92",
|
"version": "0.3.0",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ark-ui/solid": "3.3.0",
|
||||||
"@stripe/stripe-js": "1.54.1",
|
"@stripe/stripe-js": "1.54.1",
|
||||||
"@udecode/plate-common": "30.4.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"dompurify": "3.0.6",
|
"dompurify": "3.0.6",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const indexConfig = {
|
|||||||
file: 'dist/index.js',
|
file: 'dist/index.js',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
},
|
},
|
||||||
|
onwarn,
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve({ extensions }),
|
resolve({ extensions }),
|
||||||
babel({
|
babel({
|
||||||
@@ -56,4 +57,15 @@ const configs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function onwarn(warning, warn) {
|
||||||
|
if (
|
||||||
|
warning.code === 'CIRCULAR_DEPENDENCY' &&
|
||||||
|
warning.ids.some((id) => id.includes('@internationalized+date'))
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(warning.message)
|
||||||
|
}
|
||||||
|
|
||||||
export default configs
|
export default configs
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ pre {
|
|||||||
backdrop-filter: blur(var(--typebot-guest-bubble-blur));
|
backdrop-filter: blur(var(--typebot-guest-bubble-blur));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-guest-bubble-image-attachment {
|
||||||
|
border-radius: var(--typebot-guest-bubble-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
.typebot-input {
|
.typebot-input {
|
||||||
color: var(--typebot-input-color);
|
color: var(--typebot-input-color);
|
||||||
background-color: rgba(
|
background-color: rgba(
|
||||||
@@ -279,12 +283,17 @@ pre {
|
|||||||
border-radius: var(--typebot-input-border-radius);
|
border-radius: var(--typebot-input-border-radius);
|
||||||
box-shadow: var(--typebot-input-box-shadow);
|
box-shadow: var(--typebot-input-box-shadow);
|
||||||
backdrop-filter: blur(var(--typebot-input-blur));
|
backdrop-filter: blur(var(--typebot-input-blur));
|
||||||
|
transition: filter 100ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typebot-input-error-message {
|
.typebot-input-error-message {
|
||||||
color: var(--typebot-input-color);
|
color: var(--typebot-input-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-input-form .typebot-button {
|
||||||
|
box-shadow: var(--typebot-input-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.typebot-button > .send-icon {
|
.typebot-button > .send-icon {
|
||||||
fill: var(--typebot-button-color);
|
fill: var(--typebot-button-color);
|
||||||
}
|
}
|
||||||
@@ -446,3 +455,138 @@ select option {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
transition: width 0.25s ease;
|
transition: width 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInFromTop {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOutFromTop {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInFromBottom {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOutFromBottom {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='menu'][data-part='content'] {
|
||||||
|
color: var(--typebot-input-color);
|
||||||
|
background-color: rgba(
|
||||||
|
var(--typebot-input-bg-rgb),
|
||||||
|
var(--typebot-input-opacity)
|
||||||
|
);
|
||||||
|
border-width: var(--typebot-input-border-width);
|
||||||
|
border-color: rgba(
|
||||||
|
var(--typebot-input-border-rgb),
|
||||||
|
var(--typebot-input-border-opacity)
|
||||||
|
);
|
||||||
|
border-radius: var(--typebot-input-border-radius);
|
||||||
|
box-shadow: var(--typebot-input-box-shadow);
|
||||||
|
backdrop-filter: blur(var(--typebot-input-blur));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='menu'][data-part='item'] {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(
|
||||||
|
var(--typebot-input-bg-rgb),
|
||||||
|
var(--typebot-input-opacity)
|
||||||
|
);
|
||||||
|
border-radius: var(--typebot-input-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='menu'][data-part='content'][data-state='open'] {
|
||||||
|
animation: fadeInFromTop 150ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='menu'][data-part='content'][data-state='closed'] {
|
||||||
|
animation: fadeOutFromTop 50ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='group'] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='root'] {
|
||||||
|
border-radius: var(--typebot-chat-container-border-radius);
|
||||||
|
color: var(--typebot-input-color);
|
||||||
|
background-color: rgba(
|
||||||
|
var(--typebot-input-bg-rgb),
|
||||||
|
var(--typebot-input-opacity)
|
||||||
|
);
|
||||||
|
box-shadow: var(--typebot-input-box-shadow);
|
||||||
|
max-width: 60vw;
|
||||||
|
@apply flex flex-col pl-4 py-4 pr-8 gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='title'] {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='description'] {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='root'][data-state='open'] {
|
||||||
|
animation: fadeInFromBottom 150ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='toast'][data-part='root'][data-state='closed'] {
|
||||||
|
animation: fadeOutFromBottom 50ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='progress'][data-part='root'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='progress'][data-part='circle'] {
|
||||||
|
--size: 40px;
|
||||||
|
--thickness: 4px;
|
||||||
|
--radius: calc(40px / 2 - 4px / 2);
|
||||||
|
--circomference: calc(2 * 3.14159 * calc(40px / 2 - 4px / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='progress'][data-part='circle-range'] {
|
||||||
|
stroke: white;
|
||||||
|
--transition-prop: stroke-dasharray, stroke, stroke-dashoffset;
|
||||||
|
transition-property: stroke-dasharray, stroke, stroke-dashoffset;
|
||||||
|
--transition-duration: 0.2s;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-scope='progress'][data-part='circle-track'] {
|
||||||
|
stroke: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ import {
|
|||||||
defaultProgressBarPosition,
|
defaultProgressBarPosition,
|
||||||
} from '@typebot.io/schemas/features/typebot/theme/constants'
|
} from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
import { CorsError } from '@/utils/CorsError'
|
import { CorsError } from '@/utils/CorsError'
|
||||||
|
import { Toaster, Toast } from '@ark-ui/solid'
|
||||||
|
import { CloseIcon } from './icons/CloseIcon'
|
||||||
|
import { toaster } from '@/utils/toaster'
|
||||||
|
|
||||||
export type BotProps = {
|
export type BotProps = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -335,6 +338,17 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
>
|
>
|
||||||
<LiteBadge botContainer={botContainer} />
|
<LiteBadge botContainer={botContainer} />
|
||||||
</Show>
|
</Show>
|
||||||
|
<Toaster toaster={toaster}>
|
||||||
|
{(toast) => (
|
||||||
|
<Toast.Root>
|
||||||
|
<Toast.Title>{toast().title}</Toast.Title>
|
||||||
|
<Toast.Description>{toast().description}</Toast.Description>
|
||||||
|
<Toast.CloseTrigger class="absolute right-2 top-2">
|
||||||
|
<CloseIcon class="w-4 h-4" />
|
||||||
|
</Toast.CloseTrigger>
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</Toaster>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
formattedMessages,
|
formattedMessages,
|
||||||
setFormattedMessages,
|
setFormattedMessages,
|
||||||
} from '@/utils/formattedMessagesSignal'
|
} from '@/utils/formattedMessagesSignal'
|
||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
|
||||||
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
|
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
|
||||||
import { HTTPError } from 'ky'
|
import { HTTPError } from 'ky'
|
||||||
import { persist } from '@/utils/persist'
|
import { persist } from '@/utils/persist'
|
||||||
@@ -147,13 +146,6 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
const currentInputBlock = [...chatChunks()].pop()?.input
|
const currentInputBlock = [...chatChunks()].pop()?.input
|
||||||
if (currentInputBlock?.id && props.onAnswer && message)
|
if (currentInputBlock?.id && props.onAnswer && message)
|
||||||
props.onAnswer({ message, blockId: currentInputBlock.id })
|
props.onAnswer({ message, blockId: currentInputBlock.id })
|
||||||
if (currentInputBlock?.type === InputBlockType.FILE)
|
|
||||||
props.onNewLogs?.([
|
|
||||||
{
|
|
||||||
description: 'Files are not uploaded in preview mode',
|
|
||||||
status: 'info',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
const longRequest = setTimeout(() => {
|
const longRequest = setTimeout(() => {
|
||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
DateInputBlock,
|
DateInputBlock,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { BotContext, InputSubmitContent } from '@/types'
|
import { Answer, BotContext, InputSubmitContent } from '@/types'
|
||||||
import { TextInput } from '@/features/blocks/inputs/textInput'
|
import { TextInput } from '@/features/blocks/inputs/textInput'
|
||||||
import { NumberInput } from '@/features/blocks/inputs/number'
|
import { NumberInput } from '@/features/blocks/inputs/number'
|
||||||
import { EmailInput } from '@/features/blocks/inputs/email'
|
import { EmailInput } from '@/features/blocks/inputs/email'
|
||||||
@@ -48,24 +48,33 @@ type Props = {
|
|||||||
isInputPrefillEnabled: boolean
|
isInputPrefillEnabled: boolean
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
onTransitionEnd: () => void
|
onTransitionEnd: () => void
|
||||||
onSubmit: (answer: string) => void
|
onSubmit: (answer: string, attachments?: Answer['attachments']) => void
|
||||||
onSkip: () => void
|
onSkip: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InputChatBlock = (props: Props) => {
|
export const InputChatBlock = (props: Props) => {
|
||||||
const [answer, setAnswer] = persist(createSignal<string>(), {
|
const [answer, setAnswer] = persist(createSignal<Answer>(), {
|
||||||
key: `typebot-${props.context.typebot.id}-input-${props.chunkIndex}`,
|
key: `typebot-${props.context.typebot.id}-input-${props.chunkIndex}`,
|
||||||
storage: props.context.storage,
|
storage: props.context.storage,
|
||||||
})
|
})
|
||||||
const [formattedMessage, setFormattedMessage] = createSignal<string>()
|
|
||||||
|
|
||||||
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
const handleSubmit = async ({
|
||||||
setAnswer(label ?? value)
|
label,
|
||||||
props.onSubmit(value ?? label)
|
value,
|
||||||
|
attachments,
|
||||||
|
}: InputSubmitContent & Pick<Answer, 'attachments'>) => {
|
||||||
|
setAnswer({
|
||||||
|
text: props.block.type !== InputBlockType.FILE ? label ?? value : '',
|
||||||
|
attachments,
|
||||||
|
})
|
||||||
|
props.onSubmit(
|
||||||
|
value ?? label,
|
||||||
|
props.block.type === InputBlockType.FILE ? undefined : attachments
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSkip = (label: string) => {
|
const handleSkip = (label: string) => {
|
||||||
setAnswer(label)
|
setAnswer({ text: label })
|
||||||
props.onSkip()
|
props.onSkip()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +82,15 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
const formattedMessage = formattedMessages().findLast(
|
const formattedMessage = formattedMessages().findLast(
|
||||||
(message) => props.chunkIndex === message.inputIndex
|
(message) => props.chunkIndex === message.inputIndex
|
||||||
)?.formattedMessage
|
)?.formattedMessage
|
||||||
if (formattedMessage) setFormattedMessage(formattedMessage)
|
if (formattedMessage && props.block.type !== InputBlockType.FILE)
|
||||||
|
setAnswer((answer) => ({ ...answer, text: formattedMessage }))
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={answer() && !props.hasError}>
|
<Match when={answer() && !props.hasError}>
|
||||||
<GuestBubble
|
<GuestBubble
|
||||||
message={formattedMessage() ?? (answer() as string)}
|
message={answer() as Answer}
|
||||||
showAvatar={
|
showAvatar={
|
||||||
props.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
|
props.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
|
||||||
}
|
}
|
||||||
@@ -107,7 +117,7 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
block={props.block}
|
block={props.block}
|
||||||
chunkIndex={props.chunkIndex}
|
chunkIndex={props.chunkIndex}
|
||||||
isInputPrefillEnabled={props.isInputPrefillEnabled}
|
isInputPrefillEnabled={props.isInputPrefillEnabled}
|
||||||
existingAnswer={props.hasError ? answer() : undefined}
|
existingAnswer={props.hasError ? answer()?.text : undefined}
|
||||||
onTransitionEnd={props.onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onSkip={handleSkip}
|
onSkip={handleSkip}
|
||||||
@@ -147,6 +157,7 @@ const Input = (props: {
|
|||||||
<TextInput
|
<TextInput
|
||||||
block={props.block as TextInputBlock}
|
block={props.block as TextInputBlock}
|
||||||
defaultValue={getPrefilledValue()}
|
defaultValue={getPrefilledValue()}
|
||||||
|
context={props.context}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
27
packages/embeds/js/src/components/Modal.tsx
Normal file
27
packages/embeds/js/src/components/Modal.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Dialog } from '@ark-ui/solid'
|
||||||
|
import { JSX } from 'solid-js'
|
||||||
|
import { CloseIcon } from './icons/CloseIcon'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
export const Modal = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<Dialog.Root
|
||||||
|
open={props.isOpen}
|
||||||
|
lazyMount
|
||||||
|
unmountOnExit
|
||||||
|
onOpenChange={(e) => (!e.open ? props.onClose?.() : undefined)}
|
||||||
|
>
|
||||||
|
<Dialog.Backdrop class="fixed inset-0 bg-[rgba(0,0,0,0.5)] h-screen z-50" />
|
||||||
|
<Dialog.Positioner class="fixed inset-0 z-50 flex items-center justify-center px-2">
|
||||||
|
<Dialog.Content>{props.children}</Dialog.Content>
|
||||||
|
<Dialog.CloseTrigger class="fixed top-2 right-2 z-50 rounded-md bg-white p-2 text-black">
|
||||||
|
<CloseIcon class="w-6 h-6" />
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,29 +1,41 @@
|
|||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { splitProps } from 'solid-js'
|
import { splitProps, Switch, Match } from 'solid-js'
|
||||||
import { JSX } from 'solid-js/jsx-runtime'
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
import { SendIcon } from './icons'
|
import { SendIcon } from './icons'
|
||||||
import { Button } from './Button'
|
import { Button } from './Button'
|
||||||
import { isEmpty } from '@typebot.io/lib'
|
import { isEmpty } from '@typebot.io/lib'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
disableIcon?: boolean
|
disableIcon?: boolean
|
||||||
|
class?: string
|
||||||
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
export const SendButton = (props: SendButtonProps) => {
|
export const SendButton = (props: SendButtonProps) => {
|
||||||
const [local, others] = splitProps(props, ['disableIcon'])
|
const [local, others] = splitProps(props, ['disableIcon'])
|
||||||
|
const showIcon =
|
||||||
|
(isMobile() && !local.disableIcon) ||
|
||||||
|
!props.children ||
|
||||||
|
(typeof props.children === 'string' && isEmpty(props.children))
|
||||||
return (
|
return (
|
||||||
<Button type="submit" {...others}>
|
<Button
|
||||||
{(isMobile() && !local.disableIcon) ||
|
type="submit"
|
||||||
!props.children ||
|
{...others}
|
||||||
(typeof props.children === 'string' && isEmpty(props.children)) ? (
|
class={clsx('flex items-center', props.class)}
|
||||||
<SendIcon
|
aria-label={showIcon ? 'Send' : undefined}
|
||||||
class={'send-icon flex ' + (local.disableIcon ? 'hidden' : '')}
|
>
|
||||||
/>
|
<Switch>
|
||||||
) : (
|
<Match when={showIcon}>
|
||||||
props.children
|
<SendIcon
|
||||||
)}
|
class={
|
||||||
|
'send-icon flex w-6 h-6 ' + (local.disableIcon ? 'hidden' : '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!showIcon}>{props.children}</Match>
|
||||||
|
</Switch>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
103
packages/embeds/js/src/components/TextInputAddFileButton.tsx
Normal file
103
packages/embeds/js/src/components/TextInputAddFileButton.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { Show } from 'solid-js'
|
||||||
|
import { Menu } from '@ark-ui/solid'
|
||||||
|
import { CameraIcon } from './icons/CameraIcon'
|
||||||
|
import { FileIcon } from './icons/FileIcon'
|
||||||
|
import { PictureIcon } from './icons/PictureIcon'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
import { PaperClipIcon } from './icons/PaperClipIcon'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onNewFiles: (files: FileList) => void
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
export const TextInputAddFileButton = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="document-upload"
|
||||||
|
multiple
|
||||||
|
class="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.currentTarget.files) return
|
||||||
|
props.onNewFiles(e.currentTarget.files)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="photos-upload"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
class="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.currentTarget.files) return
|
||||||
|
props.onNewFiles(e.currentTarget.files)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Show when={isMobile()}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="camera-upload"
|
||||||
|
class="hidden"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
capture="environment"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.currentTarget.files) return
|
||||||
|
props.onNewFiles(e.currentTarget.files)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger
|
||||||
|
class={clsx(
|
||||||
|
'filter data-[state=open]:backdrop-brightness-90 hover:backdrop-brightness-95 transition rounded-md p-2 focus:outline-none',
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
aria-label="Add attachments"
|
||||||
|
>
|
||||||
|
<PaperClipIcon class="w-5" />
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Positioner>
|
||||||
|
<Menu.Content class="p-3 gap-2 focus:outline-none">
|
||||||
|
<Menu.Item
|
||||||
|
value="document"
|
||||||
|
asChild={(props) => (
|
||||||
|
<label
|
||||||
|
{...props()}
|
||||||
|
for="document-upload"
|
||||||
|
class="p-2 filter hover:brightness-95 flex gap-3 items-center"
|
||||||
|
>
|
||||||
|
<FileIcon class="w-4" /> Document
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
value="photos"
|
||||||
|
asChild={(props) => (
|
||||||
|
<label
|
||||||
|
{...props()}
|
||||||
|
for="photos-upload"
|
||||||
|
class="p-2 filter hover:brightness-95 flex gap-3 items-center"
|
||||||
|
>
|
||||||
|
<PictureIcon class="w-4" /> Photos & videos
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Show when={isMobile()}>
|
||||||
|
<Menu.Item
|
||||||
|
value="camera"
|
||||||
|
class="p-2 filter hover:brightness-95 flex gap-3 items-center"
|
||||||
|
>
|
||||||
|
<CameraIcon class="w-4" />
|
||||||
|
Camera
|
||||||
|
</Menu.Item>
|
||||||
|
</Show>
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Positioner>
|
||||||
|
</Menu.Root>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,33 +1,105 @@
|
|||||||
import { Show } from 'solid-js'
|
import { createSignal, For, Show } from 'solid-js'
|
||||||
import { Avatar } from '../avatars/Avatar'
|
import { Avatar } from '../avatars/Avatar'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
import { Answer } from '@/types'
|
||||||
|
import { Modal } from '../Modal'
|
||||||
|
import { isNotEmpty } from '@typebot.io/lib'
|
||||||
|
import { FilePreview } from '@/features/blocks/inputs/fileUpload/components/FilePreview'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string
|
message: Answer
|
||||||
showAvatar: boolean
|
showAvatar: boolean
|
||||||
avatarSrc?: string
|
avatarSrc?: string
|
||||||
hasHostAvatar: boolean
|
hasHostAvatar: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GuestBubble = (props: Props) => (
|
export const GuestBubble = (props: Props) => {
|
||||||
<div
|
const [clickedImageSrc, setClickedImageSrc] = createSignal<string>()
|
||||||
class="flex justify-end items-end animate-fade-in gap-2 guest-container"
|
|
||||||
style={{
|
return (
|
||||||
'margin-left': props.hasHostAvatar
|
<div
|
||||||
? isMobile()
|
class="flex justify-end items-end animate-fade-in gap-2 guest-container"
|
||||||
? '28px'
|
style={{
|
||||||
: '50px'
|
'margin-left': props.hasHostAvatar
|
||||||
: undefined,
|
? isMobile()
|
||||||
}}
|
? '28px'
|
||||||
>
|
: '50px'
|
||||||
<span
|
: undefined,
|
||||||
class="px-4 py-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
}}
|
||||||
data-testid="guest-bubble"
|
|
||||||
>
|
>
|
||||||
{props.message}
|
<div class="flex flex-col gap-1 items-end">
|
||||||
</span>
|
<Show when={(props.message.attachments ?? []).length > 0}>
|
||||||
<Show when={props.showAvatar}>
|
<div
|
||||||
<Avatar initialAvatarSrc={props.avatarSrc} />
|
class={clsx(
|
||||||
</Show>
|
'flex gap-1 overflow-auto max-w-[350px]',
|
||||||
</div>
|
isMobile() ? 'flex-wrap justify-end' : 'items-center'
|
||||||
)
|
)}
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each={props.message.attachments?.filter((attachment) =>
|
||||||
|
attachment.type.startsWith('image')
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(attachment, idx) => (
|
||||||
|
<img
|
||||||
|
src={attachment.url}
|
||||||
|
alt={`Attached image ${idx() + 1}`}
|
||||||
|
class={clsx(
|
||||||
|
'typebot-guest-bubble-image-attachment cursor-pointer',
|
||||||
|
props.message.attachments!.filter((attachment) =>
|
||||||
|
attachment.type.startsWith('image')
|
||||||
|
).length > 1 && 'max-w-[90%]'
|
||||||
|
)}
|
||||||
|
onClick={() => setClickedImageSrc(attachment.url)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex gap-1 overflow-auto max-w-[350px]',
|
||||||
|
isMobile() ? 'flex-wrap justify-end' : 'items-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each={props.message.attachments?.filter(
|
||||||
|
(attachment) => !attachment.type.startsWith('image')
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(attachment) => (
|
||||||
|
<FilePreview
|
||||||
|
file={{
|
||||||
|
name: attachment.url.split('/').at(-1)!,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class="p-[1px] whitespace-pre-wrap max-w-full typebot-guest-bubble flex flex-col"
|
||||||
|
data-testid="guest-bubble"
|
||||||
|
>
|
||||||
|
<Show when={isNotEmpty(props.message.text)}>
|
||||||
|
<span class="px-[15px] py-[7px]">{props.message.text}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={clickedImageSrc() !== undefined}
|
||||||
|
onClose={() => setClickedImageSrc(undefined)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={clickedImageSrc()}
|
||||||
|
alt="Attachment"
|
||||||
|
style={{ 'border-radius': '6px' }}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<Show when={props.showAvatar}>
|
||||||
|
<Avatar initialAvatarSrc={props.avatarSrc} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
17
packages/embeds/js/src/components/icons/CameraIcon.tsx
Normal file
17
packages/embeds/js/src/components/icons/CameraIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const CameraIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
|
||||||
|
<circle cx="12" cy="13" r="3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
17
packages/embeds/js/src/components/icons/FileIcon.tsx
Normal file
17
packages/embeds/js/src/components/icons/FileIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const FileIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||||
|
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
18
packages/embeds/js/src/components/icons/PaperClipIcon.tsx
Normal file
18
packages/embeds/js/src/components/icons/PaperClipIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const PaperClipIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
packages/embeds/js/src/components/icons/PictureIcon.tsx
Normal file
19
packages/embeds/js/src/components/icons/PictureIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const PictureIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||||
|
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||||
|
<circle cx="10" cy="12" r="2" />
|
||||||
|
<path d="m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
17
packages/embeds/js/src/components/icons/PlusIcon.tsx
Normal file
17
packages/embeds/js/src/components/icons/PlusIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const PlusIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
@@ -16,93 +16,94 @@ export const DateForm = (props: Props) => {
|
|||||||
parseDefaultValue(props.defaultValue ?? '')
|
parseDefaultValue(props.defaultValue ?? '')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (inputValues().from === '' && inputValues().to === '') return
|
||||||
|
props.onSubmit({
|
||||||
|
value: `${inputValues().from}${
|
||||||
|
props.options?.isRange ? ` to ${inputValues().to}` : ''
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col">
|
<div class="typebot-input-form flex gap-2 items-end">
|
||||||
<div class="flex items-center">
|
<form
|
||||||
<form
|
class={clsx(
|
||||||
class={clsx(
|
'flex typebot-input',
|
||||||
'flex justify-between typebot-input pr-2',
|
props.options?.isRange ? 'items-end' : 'items-center'
|
||||||
props.options?.isRange ? 'items-end' : 'items-center'
|
)}
|
||||||
)}
|
onSubmit={(e) => {
|
||||||
onSubmit={(e) => {
|
e.preventDefault()
|
||||||
if (inputValues().from === '' && inputValues().to === '') return
|
submit()
|
||||||
e.preventDefault()
|
}}
|
||||||
props.onSubmit({
|
>
|
||||||
value: `${inputValues().from}${
|
<div class="flex flex-col">
|
||||||
props.options?.isRange ? ` to ${inputValues().to}` : ''
|
<div
|
||||||
}`,
|
class={
|
||||||
})
|
'flex items-center p-4 ' +
|
||||||
}}
|
(props.options?.isRange ? 'pb-0 gap-2' : '')
|
||||||
>
|
}
|
||||||
<div class="flex flex-col">
|
>
|
||||||
<div
|
{props.options?.isRange && (
|
||||||
class={
|
<p class="font-semibold">
|
||||||
'flex items-center p-4 ' +
|
{props.options.labels?.from ??
|
||||||
(props.options?.isRange ? 'pb-0 gap-2' : '')
|
defaultDateInputOptions.labels.from}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
class="focus:outline-none flex-1 w-full text-input typebot-date-input"
|
||||||
|
style={{
|
||||||
|
'min-height': '32px',
|
||||||
|
'min-width': '100px',
|
||||||
|
'font-size': '16px',
|
||||||
|
}}
|
||||||
|
value={inputValues().from}
|
||||||
|
type={props.options?.hasTime ? 'datetime-local' : 'date'}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInputValues({
|
||||||
|
...inputValues(),
|
||||||
|
from: e.currentTarget.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
min={props.options?.min}
|
||||||
{props.options?.isRange && (
|
max={props.options?.max}
|
||||||
|
data-testid="from-date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{props.options?.isRange && (
|
||||||
|
<div class="flex items-center p-4">
|
||||||
|
{props.options.isRange && (
|
||||||
<p class="font-semibold">
|
<p class="font-semibold">
|
||||||
{props.options.labels?.from ??
|
{props.options.labels?.to ??
|
||||||
defaultDateInputOptions.labels.from}
|
defaultDateInputOptions.labels.to}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
class="focus:outline-none flex-1 w-full text-input typebot-date-input"
|
class="focus:outline-none flex-1 w-full text-input ml-2 typebot-date-input"
|
||||||
style={{
|
style={{
|
||||||
'min-height': '32px',
|
'min-height': '32px',
|
||||||
'min-width': '100px',
|
'min-width': '100px',
|
||||||
'font-size': '16px',
|
'font-size': '16px',
|
||||||
}}
|
}}
|
||||||
value={inputValues().from}
|
value={inputValues().to}
|
||||||
type={props.options?.hasTime ? 'datetime-local' : 'date'}
|
type={props.options.hasTime ? 'datetime-local' : 'date'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setInputValues({
|
setInputValues({
|
||||||
...inputValues(),
|
...inputValues(),
|
||||||
from: e.currentTarget.value,
|
to: e.currentTarget.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={props.options?.min}
|
min={props.options?.min}
|
||||||
max={props.options?.max}
|
max={props.options?.max}
|
||||||
data-testid="from-date"
|
data-testid="to-date"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{props.options?.isRange && (
|
)}
|
||||||
<div class="flex items-center p-4">
|
</div>
|
||||||
{props.options.isRange && (
|
</form>
|
||||||
<p class="font-semibold">
|
<SendButton class="h-[56px]" on:click={submit}>
|
||||||
{props.options.labels?.to ??
|
{props.options?.labels?.button}
|
||||||
defaultDateInputOptions.labels.to}
|
</SendButton>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
class="focus:outline-none flex-1 w-full text-input ml-2 typebot-date-input"
|
|
||||||
style={{
|
|
||||||
'min-height': '32px',
|
|
||||||
'min-width': '100px',
|
|
||||||
'font-size': '16px',
|
|
||||||
}}
|
|
||||||
value={inputValues().to}
|
|
||||||
type={props.options.hasTime ? 'datetime-local' : 'date'}
|
|
||||||
onChange={(e) =>
|
|
||||||
setInputValues({
|
|
||||||
...inputValues(),
|
|
||||||
to: e.currentTarget.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
min={props.options?.min}
|
|
||||||
max={props.options?.max}
|
|
||||||
data-testid="to-date"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SendButton class="my-2 ml-2">
|
|
||||||
{props.options?.labels?.button}
|
|
||||||
</SendButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,25 +49,23 @@ export const EmailInput = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'flex items-end justify-between pr-2 typebot-input w-full'}
|
class="typebot-input-form flex w-full gap-2 items-end max-w-[350px]"
|
||||||
data-testid="input"
|
|
||||||
style={{
|
|
||||||
'max-width': '350px',
|
|
||||||
}}
|
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
>
|
>
|
||||||
<ShortTextInput
|
<div class={'flex typebot-input w-full'}>
|
||||||
ref={inputRef}
|
<ShortTextInput
|
||||||
value={inputValue()}
|
ref={inputRef}
|
||||||
placeholder={
|
value={inputValue()}
|
||||||
props.block.options?.labels?.placeholder ??
|
placeholder={
|
||||||
defaultEmailInputOptions.labels.placeholder
|
props.block.options?.labels?.placeholder ??
|
||||||
}
|
defaultEmailInputOptions.labels.placeholder
|
||||||
onInput={handleInput}
|
}
|
||||||
type="email"
|
onInput={handleInput}
|
||||||
autocomplete="email"
|
type="email"
|
||||||
/>
|
autocomplete="email"
|
||||||
<SendButton type="button" class="my-2 ml-2" on:click={submit}>
|
/>
|
||||||
|
</div>
|
||||||
|
<SendButton type="button" class="h-[56px]" on:click={submit}>
|
||||||
{props.block.options?.labels?.button}
|
{props.block.options?.labels?.button}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { FileIcon } from '@/components/icons/FileIcon'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
file: { name: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilePreview = (props: Props) => {
|
||||||
|
const fileColor = getFileAssociatedColor(props.file)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex items-center gap-4 border bg-white border-gray-200 rounded-md p-2 text-gray-900 min-w-[250px]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'rounded-md text-white p-2 flex items-center',
|
||||||
|
fileColor === 'pink' && 'bg-pink-400',
|
||||||
|
fileColor === 'blue' && 'bg-blue-400',
|
||||||
|
fileColor === 'green' && 'bg-green-400',
|
||||||
|
fileColor === 'gray' && 'bg-gray-400',
|
||||||
|
fileColor === 'orange' && 'bg-orange-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileIcon class="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-md font-semibold text-sm">{props.file.name}</span>
|
||||||
|
<span class="text-gray-500 text-xs">
|
||||||
|
{formatFileExtensionHumanReadable(props.file)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileExtensionHumanReadable = (file: { name: string }) => {
|
||||||
|
const extension = file.name.split('.').pop()
|
||||||
|
switch (extension) {
|
||||||
|
case 'pdf':
|
||||||
|
return 'PDF'
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return 'Word'
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
case 'csv':
|
||||||
|
return 'Sheet'
|
||||||
|
case 'json':
|
||||||
|
return 'JSON'
|
||||||
|
case 'md':
|
||||||
|
return 'Markdown'
|
||||||
|
default:
|
||||||
|
return 'DOCUMENT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileAssociatedColor = (file: {
|
||||||
|
name: string
|
||||||
|
}): 'pink' | 'blue' | 'green' | 'gray' | 'orange' => {
|
||||||
|
const extension = file.name.split('.').pop()
|
||||||
|
if (!extension) return 'gray'
|
||||||
|
switch (extension) {
|
||||||
|
case 'pdf':
|
||||||
|
return 'pink'
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return 'blue'
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
case 'csv':
|
||||||
|
return 'green'
|
||||||
|
case 'json':
|
||||||
|
return 'orange'
|
||||||
|
default:
|
||||||
|
return 'gray'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { BotContext, InputSubmitContent } from '@/types'
|
import { BotContext, InputSubmitContent } from '@/types'
|
||||||
import { FileInputBlock } from '@typebot.io/schemas'
|
import { FileInputBlock } from '@typebot.io/schemas'
|
||||||
import { createSignal, Match, Show, Switch } from 'solid-js'
|
import { createSignal, Match, Show, Switch, For } from 'solid-js'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Spinner } from '@/components/Spinner'
|
import { Spinner } from '@/components/Spinner'
|
||||||
import { uploadFiles } from '../helpers/uploadFiles'
|
import { uploadFiles } from '../helpers/uploadFiles'
|
||||||
import { guessApiHost } from '@/utils/guessApiHost'
|
import { guessApiHost } from '@/utils/guessApiHost'
|
||||||
import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
|
|
||||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
import { SelectedFile } from './SelectedFile'
|
||||||
|
import { sanitizeNewFile } from '../helpers/sanitizeSelectedFiles'
|
||||||
|
import { toaster } from '@/utils/toaster'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
context: BotContext
|
context: BotContext
|
||||||
@@ -22,39 +24,34 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
const [isUploading, setIsUploading] = createSignal(false)
|
const [isUploading, setIsUploading] = createSignal(false)
|
||||||
const [uploadProgressPercent, setUploadProgressPercent] = createSignal(0)
|
const [uploadProgressPercent, setUploadProgressPercent] = createSignal(0)
|
||||||
const [isDraggingOver, setIsDraggingOver] = createSignal(false)
|
const [isDraggingOver, setIsDraggingOver] = createSignal(false)
|
||||||
const [errorMessage, setErrorMessage] = createSignal<string>()
|
|
||||||
|
|
||||||
const onNewFiles = (files: FileList) => {
|
const onNewFiles = (files: FileList) => {
|
||||||
setErrorMessage(undefined)
|
|
||||||
const newFiles = Array.from(files)
|
const newFiles = Array.from(files)
|
||||||
const sizeLimit =
|
.map((file) =>
|
||||||
props.block.options && 'sizeLimit' in props.block.options
|
sanitizeNewFile({
|
||||||
? props.block.options?.sizeLimit ??
|
existingFiles: selectedFiles(),
|
||||||
getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE')
|
newFile: file,
|
||||||
: undefined
|
params: {
|
||||||
if (
|
sizeLimit:
|
||||||
sizeLimit &&
|
props.block.options && 'sizeLimit' in props.block.options
|
||||||
newFiles.some((file) => file.size > sizeLimit * 1024 * 1024)
|
? props.block.options.sizeLimit
|
||||||
)
|
: undefined,
|
||||||
return setErrorMessage(`A file is larger than ${sizeLimit}MB`)
|
},
|
||||||
if (!props.block.options?.isMultipleAllowed && files)
|
onError: ({ description, title }) =>
|
||||||
|
toaster.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter(isDefined)
|
||||||
|
|
||||||
|
if (newFiles.length === 0) return
|
||||||
|
|
||||||
|
if (!props.block.options?.isMultipleAllowed)
|
||||||
return startSingleFileUpload(newFiles[0])
|
return startSingleFileUpload(newFiles[0])
|
||||||
if (selectedFiles().length === 0) {
|
|
||||||
setSelectedFiles(newFiles)
|
setSelectedFiles([...selectedFiles(), ...newFiles])
|
||||||
return
|
|
||||||
}
|
|
||||||
const parsedNewFiles = newFiles.map((newFile) => {
|
|
||||||
let fileName = newFile.name
|
|
||||||
let counter = 1
|
|
||||||
while (selectedFiles().some((file) => file.name === fileName)) {
|
|
||||||
const dotIndex = newFile.name.lastIndexOf('.')
|
|
||||||
const extension = dotIndex !== -1 ? newFile.name.slice(dotIndex) : ''
|
|
||||||
fileName = `${newFile.name.slice(0, dotIndex)}(${counter})${extension}`
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
return new File([newFile], fileName, { type: newFile.type })
|
|
||||||
})
|
|
||||||
setSelectedFiles([...selectedFiles(), ...parsedNewFiles])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: SubmitEvent) => {
|
const handleSubmit = async (e: SubmitEvent) => {
|
||||||
@@ -64,13 +61,6 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startSingleFileUpload = async (file: File) => {
|
const startSingleFileUpload = async (file: File) => {
|
||||||
if (props.context.isPreview || !props.context.resultId)
|
|
||||||
return props.onSubmit({
|
|
||||||
label:
|
|
||||||
props.block.options?.labels?.success?.single ??
|
|
||||||
defaultFileInputOptions.labels.success.single,
|
|
||||||
value: 'http://fake-upload-url.com',
|
|
||||||
})
|
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
apiHost:
|
apiHost:
|
||||||
@@ -86,31 +76,17 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
if (urls.length)
|
if (urls.length && urls[0])
|
||||||
return props.onSubmit({
|
return props.onSubmit({
|
||||||
label:
|
label:
|
||||||
props.block.options?.labels?.success?.single ??
|
props.block.options?.labels?.success?.single ??
|
||||||
defaultFileInputOptions.labels.success.single,
|
defaultFileInputOptions.labels.success.single,
|
||||||
value: urls[0] ? encodeUrl(urls[0]) : '',
|
value: urls[0] ? encodeUrl(urls[0].url) : '',
|
||||||
|
attachments: [{ type: file.type, url: urls[0]!.url }],
|
||||||
})
|
})
|
||||||
setErrorMessage('An error occured while uploading the file')
|
toaster.create({ description: 'An error occured while uploading the file' })
|
||||||
}
|
}
|
||||||
const startFilesUpload = async (files: File[]) => {
|
const startFilesUpload = async (files: File[]) => {
|
||||||
const resultId = props.context.resultId
|
|
||||||
if (props.context.isPreview || !resultId)
|
|
||||||
return props.onSubmit({
|
|
||||||
label:
|
|
||||||
files.length > 1
|
|
||||||
? (
|
|
||||||
props.block.options?.labels?.success?.multiple ??
|
|
||||||
defaultFileInputOptions.labels.success.multiple
|
|
||||||
).replaceAll('{total}', files.length.toString())
|
|
||||||
: props.block.options?.labels?.success?.single ??
|
|
||||||
defaultFileInputOptions.labels.success.single,
|
|
||||||
value: files
|
|
||||||
.map((_, idx) => `http://fake-upload-url.com/${idx}`)
|
|
||||||
.join(', '),
|
|
||||||
})
|
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
apiHost:
|
apiHost:
|
||||||
@@ -127,7 +103,9 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
setUploadProgressPercent(0)
|
setUploadProgressPercent(0)
|
||||||
if (urls.length !== files.length)
|
if (urls.length !== files.length)
|
||||||
return setErrorMessage('An error occured while uploading the files')
|
return toaster.create({
|
||||||
|
description: 'An error occured while uploading the files',
|
||||||
|
})
|
||||||
props.onSubmit({
|
props.onSubmit({
|
||||||
label:
|
label:
|
||||||
urls.length > 1
|
urls.length > 1
|
||||||
@@ -137,7 +115,11 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
).replaceAll('{total}', urls.length.toString())
|
).replaceAll('{total}', urls.length.toString())
|
||||||
: props.block.options?.labels?.success?.single ??
|
: props.block.options?.labels?.success?.single ??
|
||||||
defaultFileInputOptions.labels.success.single,
|
defaultFileInputOptions.labels.success.single,
|
||||||
value: urls.filter(isDefined).map(encodeUrl).join(', '),
|
value: urls
|
||||||
|
.filter(isDefined)
|
||||||
|
.map(({ url }) => encodeUrl(url))
|
||||||
|
.join(', '),
|
||||||
|
attachments: urls.filter(isDefined),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +144,12 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
props.block.options?.labels?.skip ?? defaultFileInputOptions.labels.skip
|
props.block.options?.labels?.skip ?? defaultFileInputOptions.labels.skip
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const removeSelectedFile = (index: number) => {
|
||||||
|
setSelectedFiles((selectedFiles) =>
|
||||||
|
selectedFiles.filter((_, i) => i !== index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form class="flex flex-col w-full gap-2" onSubmit={handleSubmit}>
|
<form class="flex flex-col w-full gap-2" onSubmit={handleSubmit}>
|
||||||
<label
|
<label
|
||||||
@@ -192,17 +180,24 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={!isUploading()}>
|
<Match when={!isUploading()}>
|
||||||
<>
|
<>
|
||||||
<div class="flex flex-col justify-center items-center">
|
<div class="flex flex-col justify-center items-center gap-4 max-w-[90%]">
|
||||||
<Show when={selectedFiles().length} fallback={<UploadIcon />}>
|
<Show when={selectedFiles().length} fallback={<UploadIcon />}>
|
||||||
<span class="relative">
|
<div
|
||||||
<FileIcon />
|
class="p-4 flex gap-2 border-gray-200 border overflow-auto bg-white rounded-md w-full"
|
||||||
<div
|
on:click={(e) => {
|
||||||
class="total-files-indicator flex items-center justify-center absolute -right-1 rounded-full px-1 w-4 h-4"
|
e.preventDefault()
|
||||||
style={{ bottom: '5px' }}
|
e.stopPropagation()
|
||||||
>
|
}}
|
||||||
{selectedFiles().length}
|
>
|
||||||
</div>
|
<For each={selectedFiles()}>
|
||||||
</span>
|
{(file, index) => (
|
||||||
|
<SelectedFile
|
||||||
|
file={file}
|
||||||
|
onRemoveClick={() => removeSelectedFile(index())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<p
|
<p
|
||||||
class="text-sm text-gray-500 text-center"
|
class="text-sm text-gray-500 text-center"
|
||||||
@@ -269,9 +264,6 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={errorMessage()}>
|
|
||||||
<p class="text-red-500 text-sm">{errorMessage()}</p>
|
|
||||||
</Show>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -287,7 +279,7 @@ const UploadIcon = () => (
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="mb-3 text-gray-500"
|
class="text-gray-500"
|
||||||
>
|
>
|
||||||
<polyline points="16 16 12 12 8 16" />
|
<polyline points="16 16 12 12 8 16" />
|
||||||
<line x1="12" y1="12" x2="12" y2="21" />
|
<line x1="12" y1="12" x2="12" y2="21" />
|
||||||
@@ -296,24 +288,6 @@ const UploadIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const FileIcon = () => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="mb-3 text-gray-500"
|
|
||||||
>
|
|
||||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
|
||||||
<polyline points="13 2 13 9 20 9" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const encodeUrl = (url: string): string => {
|
const encodeUrl = (url: string): string => {
|
||||||
const fileName = url.split('/').pop()
|
const fileName = url.split('/').pop()
|
||||||
if (!fileName) return url
|
if (!fileName) return url
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
Switch,
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
createSignal,
|
||||||
|
createEffect,
|
||||||
|
onCleanup,
|
||||||
|
} from 'solid-js'
|
||||||
|
import { CloseIcon } from '@/components/icons/CloseIcon'
|
||||||
|
import { FilePreview } from './FilePreview'
|
||||||
|
import { Progress } from '@ark-ui/solid'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
|
||||||
|
export const SelectedFile = (props: {
|
||||||
|
file: File
|
||||||
|
uploadProgressPercent?: number
|
||||||
|
onRemoveClick: () => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div class="relative group flex-shrink-0">
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.file.type.startsWith('image')}>
|
||||||
|
<img
|
||||||
|
src={URL.createObjectURL(props.file)}
|
||||||
|
alt={props.file.name}
|
||||||
|
class="rounded-md object-cover w-[58px] h-[58px]"
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<FilePreview file={props.file} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="absolute -right-2 p-0.5 -top-2 rounded-full bg-gray-200 text-black border border-gray-400 opacity-1 sm:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
on:click={props.onRemoveClick}
|
||||||
|
aria-label="Remove attachment"
|
||||||
|
>
|
||||||
|
<CloseIcon class="w-4" />
|
||||||
|
</button>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
isDefined(props.uploadProgressPercent) &&
|
||||||
|
props.uploadProgressPercent !== 100
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UploadOverlay progressPercent={props.uploadProgressPercent} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UploadOverlay = (props: { progressPercent?: number }) => {
|
||||||
|
const [progressPercent, setProgressPercent] = createSignal(
|
||||||
|
props.progressPercent ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.progressPercent === 20) {
|
||||||
|
const incrementProgress = () => {
|
||||||
|
if (progressPercent() < 100) {
|
||||||
|
setProgressPercent(
|
||||||
|
(prev) => prev + (Math.floor(Math.random() * 10) + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = setInterval(incrementProgress, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="absolute w-full h-full inset-0 bg-black/20 rounded-md">
|
||||||
|
<Progress.Root
|
||||||
|
value={progressPercent()}
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Progress.Circle>
|
||||||
|
<Progress.CircleTrack />
|
||||||
|
<Progress.CircleRange />
|
||||||
|
</Progress.Circle>
|
||||||
|
</Progress.Root>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
newFile: File
|
||||||
|
existingFiles: File[]
|
||||||
|
params: {
|
||||||
|
sizeLimit?: number
|
||||||
|
}
|
||||||
|
onError: (message: { title?: string; description: string }) => void
|
||||||
|
}
|
||||||
|
export const sanitizeNewFile = ({
|
||||||
|
newFile,
|
||||||
|
existingFiles,
|
||||||
|
params,
|
||||||
|
onError,
|
||||||
|
}: Props): File | undefined => {
|
||||||
|
const sizeLimit =
|
||||||
|
params.sizeLimit ??
|
||||||
|
getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE')
|
||||||
|
|
||||||
|
if (sizeLimit && newFile.size > sizeLimit * 1024 * 1024) {
|
||||||
|
onError({
|
||||||
|
title: 'File too large',
|
||||||
|
description: `${newFile.name} is larger than ${sizeLimit}MB`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingFiles.length === 0) return newFile
|
||||||
|
|
||||||
|
let fileName = newFile.name
|
||||||
|
let counter = 1
|
||||||
|
while (existingFiles.some((file) => file.name === fileName)) {
|
||||||
|
const dotIndex = newFile.name.lastIndexOf('.')
|
||||||
|
const extension = dotIndex !== -1 ? newFile.name.slice(dotIndex) : ''
|
||||||
|
fileName = `${newFile.name.slice(0, dotIndex)}(${counter})${extension}`
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
return new File([newFile], fileName, { type: newFile.type })
|
||||||
|
}
|
||||||
@@ -9,20 +9,24 @@ type UploadFileProps = {
|
|||||||
fileName: string
|
fileName: string
|
||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
onUploadProgress?: (percent: number) => void
|
onUploadProgress?: (props: { fileIndex: number; progress: number }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type UrlList = (string | null)[]
|
type UrlList = ({
|
||||||
|
url: string
|
||||||
|
type: string
|
||||||
|
} | null)[]
|
||||||
|
|
||||||
export const uploadFiles = async ({
|
export const uploadFiles = async ({
|
||||||
apiHost,
|
apiHost,
|
||||||
files,
|
files,
|
||||||
onUploadProgress,
|
onUploadProgress,
|
||||||
}: UploadFileProps): Promise<UrlList> => {
|
}: UploadFileProps): Promise<UrlList> => {
|
||||||
const urls = []
|
const urls: UrlList = []
|
||||||
let i = 0
|
let i = 0
|
||||||
for (const { input, file } of files) {
|
for (const { input, file } of files) {
|
||||||
onUploadProgress && onUploadProgress((i / files.length) * 100)
|
onUploadProgress &&
|
||||||
|
onUploadProgress({ progress: (i / files.length) * 100, fileIndex: i })
|
||||||
i += 1
|
i += 1
|
||||||
const { data } = await sendRequest<{
|
const { data } = await sendRequest<{
|
||||||
presignedUrl: string
|
presignedUrl: string
|
||||||
@@ -52,7 +56,7 @@ export const uploadFiles = async ({
|
|||||||
|
|
||||||
if (!upload.ok) continue
|
if (!upload.ok) continue
|
||||||
|
|
||||||
urls.push(data.fileUrl)
|
urls.push({ url: data.fileUrl, type: file.type })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urls
|
return urls
|
||||||
|
|||||||
@@ -52,34 +52,32 @@ export const NumberInput = (props: NumberInputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'flex items-end justify-between pr-2 typebot-input w-full'}
|
class="typebot-input-form flex w-full gap-2 items-end max-w-[350px]"
|
||||||
data-testid="input"
|
|
||||||
style={{
|
|
||||||
'max-width': '350px',
|
|
||||||
}}
|
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
>
|
>
|
||||||
<input
|
<div class={'flex typebot-input w-full'}>
|
||||||
ref={inputRef}
|
<input
|
||||||
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
ref={inputRef}
|
||||||
style={{ 'font-size': '16px', appearance: 'auto' }}
|
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||||
value={staticValue}
|
style={{ 'font-size': '16px', appearance: 'auto' }}
|
||||||
// @ts-expect-error not defined
|
value={staticValue}
|
||||||
// eslint-disable-next-line solid/jsx-no-undef
|
// @ts-expect-error not defined
|
||||||
use:bindValue
|
// eslint-disable-next-line solid/jsx-no-undef
|
||||||
placeholder={
|
use:bindValue
|
||||||
props.block.options?.labels?.placeholder ??
|
placeholder={
|
||||||
defaultNumberInputOptions.labels.placeholder
|
props.block.options?.labels?.placeholder ??
|
||||||
}
|
defaultNumberInputOptions.labels.placeholder
|
||||||
onInput={(e) => {
|
}
|
||||||
setInputValue(targetValue(e.currentTarget))
|
onInput={(e) => {
|
||||||
}}
|
setInputValue(targetValue(e.currentTarget))
|
||||||
type="number"
|
}}
|
||||||
min={props.block.options?.min}
|
type="number"
|
||||||
max={props.block.options?.max}
|
min={props.block.options?.min}
|
||||||
step={props.block.options?.step ?? 'any'}
|
max={props.block.options?.max}
|
||||||
/>
|
step={props.block.options?.step ?? 'any'}
|
||||||
<SendButton type="button" class="my-2 ml-2" on:click={submit}>
|
/>
|
||||||
|
</div>
|
||||||
|
<SendButton type="button" class="h-[56px]" on:click={submit}>
|
||||||
{props.block.options?.labels?.button}
|
{props.block.options?.labels?.button}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -106,14 +106,10 @@ export const PhoneInput = (props: PhoneInputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'flex items-end justify-between pr-2 typebot-input'}
|
class="typebot-input-form flex w-full gap-2 items-end max-w-[350px]"
|
||||||
data-testid="input"
|
|
||||||
style={{
|
|
||||||
'max-width': '400px',
|
|
||||||
}}
|
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class={'flex typebot-input w-full'}>
|
||||||
<div class="relative typebot-country-select flex justify-center items-center">
|
<div class="relative typebot-country-select flex justify-center items-center">
|
||||||
<div class="pl-2 pr-1 flex items-center gap-2">
|
<div class="pl-2 pr-1 flex items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
@@ -156,8 +152,7 @@ export const PhoneInput = (props: PhoneInputProps) => {
|
|||||||
autofocus={!isMobile()}
|
autofocus={!isMobile()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<SendButton type="button" class="h-[56px]" on:click={submit}>
|
||||||
<SendButton type="button" class="my-2 ml-2" on:click={submit}>
|
|
||||||
{props.labels?.button}
|
{props.labels?.button}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
import { Textarea, ShortTextInput } from '@/components'
|
import { Textarea, ShortTextInput } from '@/components'
|
||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { CommandData } from '@/features/commands'
|
import { CommandData } from '@/features/commands'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { Answer, BotContext, InputSubmitContent } from '@/types'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import type { TextInputBlock } from '@typebot.io/schemas'
|
import type { TextInputBlock } from '@typebot.io/schemas'
|
||||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { TextInputAddFileButton } from '@/components/TextInputAddFileButton'
|
||||||
|
import { SelectedFile } from '../../fileUpload/components/SelectedFile'
|
||||||
|
import { sanitizeNewFile } from '../../fileUpload/helpers/sanitizeSelectedFiles'
|
||||||
|
import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
|
||||||
|
import { toaster } from '@/utils/toaster'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
import { uploadFiles } from '../../fileUpload/helpers/uploadFiles'
|
||||||
|
import { guessApiHost } from '@/utils/guessApiHost'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: TextInputBlock
|
block: TextInputBlock
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
context: BotContext
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInput = (props: Props) => {
|
export const TextInput = (props: Props) => {
|
||||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||||
|
const [selectedFiles, setSelectedFiles] = createSignal<File[]>([])
|
||||||
|
const [uploadProgress, setUploadProgress] = createSignal<
|
||||||
|
{ fileIndex: number; progress: number } | undefined
|
||||||
|
>(undefined)
|
||||||
|
const [isDraggingOver, setIsDraggingOver] = createSignal(false)
|
||||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||||
|
|
||||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||||
@@ -23,10 +37,30 @@ export const TextInput = (props: Props) => {
|
|||||||
const checkIfInputIsValid = () =>
|
const checkIfInputIsValid = () =>
|
||||||
inputRef?.value !== '' && inputRef?.reportValidity()
|
inputRef?.value !== '' && inputRef?.reportValidity()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = async () => {
|
||||||
if (checkIfInputIsValid())
|
if (checkIfInputIsValid()) {
|
||||||
props.onSubmit({ value: inputRef?.value ?? inputValue() })
|
let attachments: Answer['attachments']
|
||||||
else inputRef?.focus()
|
if (selectedFiles().length > 0) {
|
||||||
|
setUploadProgress(undefined)
|
||||||
|
const urls = await uploadFiles({
|
||||||
|
apiHost:
|
||||||
|
props.context.apiHost ?? guessApiHost({ ignoreChatApiUrl: true }),
|
||||||
|
files: selectedFiles().map((file) => ({
|
||||||
|
file: file,
|
||||||
|
input: {
|
||||||
|
sessionId: props.context.sessionId,
|
||||||
|
fileName: file.name,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
onUploadProgress: setUploadProgress,
|
||||||
|
})
|
||||||
|
attachments = urls?.filter(isDefined)
|
||||||
|
}
|
||||||
|
props.onSubmit({
|
||||||
|
value: inputRef?.value ?? inputValue(),
|
||||||
|
attachments,
|
||||||
|
})
|
||||||
|
} else inputRef?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitWhenEnter = (e: KeyboardEvent) => {
|
const submitWhenEnter = (e: KeyboardEvent) => {
|
||||||
@@ -57,41 +91,139 @@ export const TextInput = (props: Props) => {
|
|||||||
if (data.command === 'setInputValue') setInputValue(data.value)
|
if (data.command === 'setInputValue') setInputValue(data.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDraggingOver(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = () => setIsDraggingOver(false)
|
||||||
|
|
||||||
|
const handleDropFile = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!e.dataTransfer?.files) return
|
||||||
|
onNewFiles(e.dataTransfer.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNewFiles = (files: FileList) => {
|
||||||
|
const newFiles = Array.from(files)
|
||||||
|
.map((file) =>
|
||||||
|
sanitizeNewFile({
|
||||||
|
existingFiles: selectedFiles(),
|
||||||
|
newFile: file,
|
||||||
|
params: {
|
||||||
|
sizeLimit: getRuntimeVariable(
|
||||||
|
'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
onError: ({ description, title }) => {
|
||||||
|
toaster.create({
|
||||||
|
description,
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter(isDefined)
|
||||||
|
|
||||||
|
if (newFiles.length === 0) return
|
||||||
|
|
||||||
|
setSelectedFiles((selectedFiles) => [...newFiles, ...selectedFiles])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSelectedFile = (index: number) => {
|
||||||
|
setSelectedFiles((selectedFiles) =>
|
||||||
|
selectedFiles.filter((_, i) => i !== index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class="typebot-input-form flex w-full gap-2 items-end max-w-[350px]"
|
||||||
'flex justify-between pr-2 typebot-input w-full',
|
|
||||||
props.block.options?.isLong ? 'items-end' : 'items-center'
|
|
||||||
)}
|
|
||||||
data-testid="input"
|
|
||||||
style={{
|
|
||||||
'max-width': props.block.options?.isLong ? undefined : '350px',
|
|
||||||
}}
|
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
|
onDrop={handleDropFile}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
{props.block.options?.isLong ? (
|
<div
|
||||||
<Textarea
|
class={clsx(
|
||||||
ref={inputRef as HTMLTextAreaElement}
|
'typebot-input flex-col w-full',
|
||||||
onInput={handleInput}
|
isDraggingOver() && 'filter brightness-95'
|
||||||
onKeyDown={submitIfCtrlEnter}
|
)}
|
||||||
value={inputValue()}
|
>
|
||||||
placeholder={
|
<Show when={selectedFiles().length}>
|
||||||
props.block.options?.labels?.placeholder ??
|
<div
|
||||||
defaultTextInputOptions.labels.placeholder
|
class="p-2 flex gap-2 border-gray-100 overflow-auto"
|
||||||
}
|
style={{ 'border-bottom-width': '1px' }}
|
||||||
/>
|
>
|
||||||
) : (
|
<For each={selectedFiles()}>
|
||||||
<ShortTextInput
|
{(file, index) => (
|
||||||
ref={inputRef as HTMLInputElement}
|
<SelectedFile
|
||||||
onInput={handleInput}
|
file={file}
|
||||||
value={inputValue()}
|
uploadProgressPercent={
|
||||||
placeholder={
|
uploadProgress()
|
||||||
props.block.options?.labels?.placeholder ??
|
? uploadProgress()?.fileIndex === index()
|
||||||
defaultTextInputOptions.labels.placeholder
|
? 20
|
||||||
}
|
: index() < (uploadProgress()?.fileIndex ?? 0)
|
||||||
/>
|
? 100
|
||||||
)}
|
: 0
|
||||||
<SendButton type="button" class="my-2 ml-2" on:click={submit}>
|
: undefined
|
||||||
|
}
|
||||||
|
onRemoveClick={() => removeSelectedFile(index())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex justify-between px-2',
|
||||||
|
props.block.options?.isLong ? 'items-end' : 'items-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.block.options?.isLong ? (
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef as HTMLTextAreaElement}
|
||||||
|
onInput={handleInput}
|
||||||
|
onKeyDown={submitIfCtrlEnter}
|
||||||
|
value={inputValue()}
|
||||||
|
placeholder={
|
||||||
|
props.block.options?.labels?.placeholder ??
|
||||||
|
defaultTextInputOptions.labels.placeholder
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ShortTextInput
|
||||||
|
ref={inputRef as HTMLInputElement}
|
||||||
|
onInput={handleInput}
|
||||||
|
value={inputValue()}
|
||||||
|
placeholder={
|
||||||
|
props.block.options?.labels?.placeholder ??
|
||||||
|
defaultTextInputOptions.labels.placeholder
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
(props.block.options?.attachments?.isEnabled ??
|
||||||
|
defaultTextInputOptions.attachments.isEnabled) &&
|
||||||
|
props.block.options?.attachments?.saveVariableId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInputAddFileButton
|
||||||
|
onNewFiles={onNewFiles}
|
||||||
|
class={clsx(props.block.options?.isLong ? 'ml-2' : undefined)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SendButton
|
||||||
|
type="button"
|
||||||
|
on:click={submit}
|
||||||
|
isDisabled={Boolean(uploadProgress())}
|
||||||
|
class="h-[56px]"
|
||||||
|
>
|
||||||
{props.block.options?.labels?.button}
|
{props.block.options?.labels?.button}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,25 +56,23 @@ export const UrlInput = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'flex items-end justify-between pr-2 typebot-input w-full'}
|
class="typebot-input-form flex w-full gap-2 items-end max-w-[350px]"
|
||||||
data-testid="input"
|
|
||||||
style={{
|
|
||||||
'max-width': '350px',
|
|
||||||
}}
|
|
||||||
onKeyDown={submitWhenEnter}
|
onKeyDown={submitWhenEnter}
|
||||||
>
|
>
|
||||||
<ShortTextInput
|
<div class={'flex typebot-input w-full'}>
|
||||||
ref={inputRef as HTMLInputElement}
|
<ShortTextInput
|
||||||
value={inputValue()}
|
ref={inputRef as HTMLInputElement}
|
||||||
placeholder={
|
value={inputValue()}
|
||||||
props.block.options?.labels?.placeholder ??
|
placeholder={
|
||||||
defaultUrlInputOptions.labels.placeholder
|
props.block.options?.labels?.placeholder ??
|
||||||
}
|
defaultUrlInputOptions.labels.placeholder
|
||||||
onInput={handleInput}
|
}
|
||||||
type="url"
|
onInput={handleInput}
|
||||||
autocomplete="url"
|
type="url"
|
||||||
/>
|
autocomplete="url"
|
||||||
<SendButton type="button" class="my-2 ml-2" on:click={submit}>
|
/>
|
||||||
|
</div>
|
||||||
|
<SendButton type="button" class="h-[56px]" on:click={submit}>
|
||||||
{props.block.options?.labels?.button}
|
{props.block.options?.labels?.button}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
removeBotOpenedStateInStorage,
|
removeBotOpenedStateInStorage,
|
||||||
setBotOpenedStateInStorage,
|
setBotOpenedStateInStorage,
|
||||||
} from '@/utils/storage'
|
} from '@/utils/storage'
|
||||||
|
import { EnvironmentProvider } from '@ark-ui/solid'
|
||||||
|
|
||||||
export type BubbleProps = BotProps &
|
export type BubbleProps = BotProps &
|
||||||
BubbleParams & {
|
BubbleParams & {
|
||||||
@@ -157,59 +158,65 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={isMounted()}>
|
<Show when={isMounted()}>
|
||||||
<style>{styles}</style>
|
<EnvironmentProvider
|
||||||
<Show when={isPreviewMessageDisplayed()}>
|
value={document.querySelector('typebot-bubble')?.shadowRoot as Node}
|
||||||
<PreviewMessage
|
|
||||||
{...previewMessage()}
|
|
||||||
placement={bubbleProps.theme?.placement}
|
|
||||||
previewMessageTheme={bubbleProps.theme?.previewMessage}
|
|
||||||
buttonSize={buttonSize()}
|
|
||||||
onClick={handlePreviewMessageClick}
|
|
||||||
onCloseClick={hideMessage}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<BubbleButton
|
|
||||||
{...bubbleProps.theme?.button}
|
|
||||||
placement={bubbleProps.theme?.placement}
|
|
||||||
toggleBot={toggleBot}
|
|
||||||
isBotOpened={isBotOpened()}
|
|
||||||
buttonSize={buttonSize()}
|
|
||||||
/>
|
|
||||||
<div ref={progressBarContainerRef} />
|
|
||||||
<div
|
|
||||||
part="bot"
|
|
||||||
style={{
|
|
||||||
height: `calc(100% - ${buttonSize()} - 32px)`,
|
|
||||||
'max-height': props.theme?.chatWindow?.maxHeight ?? '704px',
|
|
||||||
'max-width': props.theme?.chatWindow?.maxWidth ?? '400px',
|
|
||||||
transition:
|
|
||||||
'transform 200ms cubic-bezier(0, 1.2, 1, 1), opacity 150ms ease-out',
|
|
||||||
'transform-origin':
|
|
||||||
props.theme?.placement === 'left' ? 'bottom left' : 'bottom right',
|
|
||||||
transform: isBotOpened() ? 'scale3d(1, 1, 1)' : 'scale3d(0, 0, 1)',
|
|
||||||
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
|
||||||
'background-color': bubbleProps.theme?.chatWindow?.backgroundColor,
|
|
||||||
'z-index': 42424242,
|
|
||||||
bottom: `calc(${buttonSize()} + 32px)`,
|
|
||||||
}}
|
|
||||||
class={
|
|
||||||
'fixed rounded-lg w-full' +
|
|
||||||
(isBotOpened() ? ' opacity-1' : ' opacity-0 pointer-events-none') +
|
|
||||||
(props.theme?.placement === 'left'
|
|
||||||
? ' left-5'
|
|
||||||
: ' sm:right-5 right-0')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Show when={isBotStarted()}>
|
<style>{styles}</style>
|
||||||
<Bot
|
<Show when={isPreviewMessageDisplayed()}>
|
||||||
{...botProps}
|
<PreviewMessage
|
||||||
onChatStatePersisted={handleOnChatStatePersisted}
|
{...previewMessage()}
|
||||||
prefilledVariables={prefilledVariables()}
|
placement={bubbleProps.theme?.placement}
|
||||||
class="rounded-lg"
|
previewMessageTheme={bubbleProps.theme?.previewMessage}
|
||||||
progressBarRef={progressBarContainerRef}
|
buttonSize={buttonSize()}
|
||||||
|
onClick={handlePreviewMessageClick}
|
||||||
|
onCloseClick={hideMessage}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
<BubbleButton
|
||||||
|
{...bubbleProps.theme?.button}
|
||||||
|
placement={bubbleProps.theme?.placement}
|
||||||
|
toggleBot={toggleBot}
|
||||||
|
isBotOpened={isBotOpened()}
|
||||||
|
buttonSize={buttonSize()}
|
||||||
|
/>
|
||||||
|
<div ref={progressBarContainerRef} />
|
||||||
|
<div
|
||||||
|
part="bot"
|
||||||
|
style={{
|
||||||
|
height: `calc(100% - ${buttonSize()} - 32px)`,
|
||||||
|
'max-height': props.theme?.chatWindow?.maxHeight ?? '704px',
|
||||||
|
'max-width': props.theme?.chatWindow?.maxWidth ?? '400px',
|
||||||
|
transition:
|
||||||
|
'transform 200ms cubic-bezier(0, 1.2, 1, 1), opacity 150ms ease-out',
|
||||||
|
'transform-origin':
|
||||||
|
props.theme?.placement === 'left'
|
||||||
|
? 'bottom left'
|
||||||
|
: 'bottom right',
|
||||||
|
transform: isBotOpened() ? 'scale3d(1, 1, 1)' : 'scale3d(0, 0, 1)',
|
||||||
|
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
||||||
|
'background-color': bubbleProps.theme?.chatWindow?.backgroundColor,
|
||||||
|
'z-index': 42424242,
|
||||||
|
bottom: `calc(${buttonSize()} + 32px)`,
|
||||||
|
}}
|
||||||
|
class={
|
||||||
|
'fixed rounded-lg w-full' +
|
||||||
|
(isBotOpened() ? ' opacity-1' : ' opacity-0 pointer-events-none') +
|
||||||
|
(props.theme?.placement === 'left'
|
||||||
|
? ' left-5'
|
||||||
|
: ' sm:right-5 right-0')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show when={isBotStarted()}>
|
||||||
|
<Bot
|
||||||
|
{...botProps}
|
||||||
|
onChatStatePersisted={handleOnChatStatePersisted}
|
||||||
|
prefilledVariables={prefilledVariables()}
|
||||||
|
class="rounded-lg"
|
||||||
|
progressBarRef={progressBarContainerRef}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</EnvironmentProvider>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
removeBotOpenedStateInStorage,
|
removeBotOpenedStateInStorage,
|
||||||
setBotOpenedStateInStorage,
|
setBotOpenedStateInStorage,
|
||||||
} from '@/utils/storage'
|
} from '@/utils/storage'
|
||||||
|
import { EnvironmentProvider } from '@ark-ui/solid'
|
||||||
|
|
||||||
export type PopupProps = BotProps &
|
export type PopupProps = BotProps &
|
||||||
PopupParams & {
|
PopupParams & {
|
||||||
@@ -118,44 +119,48 @@ export const Popup = (props: PopupProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={isBotOpened()}>
|
<Show when={isBotOpened()}>
|
||||||
<style>{styles}</style>
|
<EnvironmentProvider
|
||||||
<div
|
value={document.querySelector('typebot-popup')?.shadowRoot as Node}
|
||||||
class="relative"
|
|
||||||
aria-labelledby="modal-title"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
style={{
|
|
||||||
'z-index': props.theme?.zIndex ?? 42424242,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<style>{styles}</style>
|
<style>{styles}</style>
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity animate-fade-in"
|
class="relative"
|
||||||
part="overlay"
|
aria-labelledby="modal-title"
|
||||||
/>
|
role="dialog"
|
||||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
aria-modal="true"
|
||||||
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
style={{
|
||||||
<div
|
'z-index': props.theme?.zIndex ?? 42424242,
|
||||||
class={
|
}}
|
||||||
'relative h-[80vh] transform overflow-hidden rounded-lg text-left transition-all sm:my-8 w-full max-w-lg' +
|
>
|
||||||
(props.theme?.backgroundColor ? ' shadow-xl' : '')
|
<style>{styles}</style>
|
||||||
}
|
<div
|
||||||
style={{
|
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity animate-fade-in"
|
||||||
'background-color':
|
part="overlay"
|
||||||
props.theme?.backgroundColor ?? 'transparent',
|
/>
|
||||||
'max-width': props.theme?.width ?? '512px',
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
}}
|
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
on:pointerdown={stopPropagation}
|
<div
|
||||||
>
|
class={
|
||||||
<Bot
|
'relative h-[80vh] transform overflow-hidden rounded-lg text-left transition-all sm:my-8 w-full max-w-lg' +
|
||||||
{...botProps}
|
(props.theme?.backgroundColor ? ' shadow-xl' : '')
|
||||||
prefilledVariables={prefilledVariables()}
|
}
|
||||||
onChatStatePersisted={handleOnChatStatePersisted}
|
style={{
|
||||||
/>
|
'background-color':
|
||||||
|
props.theme?.backgroundColor ?? 'transparent',
|
||||||
|
'max-width': props.theme?.width ?? '512px',
|
||||||
|
}}
|
||||||
|
on:pointerdown={stopPropagation}
|
||||||
|
>
|
||||||
|
<Bot
|
||||||
|
{...botProps}
|
||||||
|
prefilledVariables={prefilledVariables()}
|
||||||
|
onChatStatePersisted={handleOnChatStatePersisted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</EnvironmentProvider>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import styles from '../../../assets/index.css'
|
|||||||
import { Bot, BotProps } from '@/components/Bot'
|
import { Bot, BotProps } from '@/components/Bot'
|
||||||
import { CommandData } from '@/features/commands/types'
|
import { CommandData } from '@/features/commands/types'
|
||||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
import { EnvironmentProvider } from '@ark-ui/solid'
|
||||||
|
|
||||||
const hostElementCss = `
|
const hostElementCss = `
|
||||||
:host {
|
:host {
|
||||||
@@ -42,7 +43,9 @@ export const Standard = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EnvironmentProvider
|
||||||
|
value={document.querySelector('typebot-standard')?.shadowRoot as Node}
|
||||||
|
>
|
||||||
<style>
|
<style>
|
||||||
{styles}
|
{styles}
|
||||||
{hostElementCss}
|
{hostElementCss}
|
||||||
@@ -50,6 +53,6 @@ export const Standard = (
|
|||||||
<Show when={isBotDisplayed()}>
|
<Show when={isBotDisplayed()}>
|
||||||
<Bot {...props} />
|
<Bot {...props} />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</EnvironmentProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ContinueChatResponse, StartChatResponse } from '@typebot.io/schemas'
|
|||||||
export type InputSubmitContent = {
|
export type InputSubmitContent = {
|
||||||
label?: string
|
label?: string
|
||||||
value: string
|
value: string
|
||||||
|
attachments?: Answer['attachments']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BotContext = {
|
export type BotContext = {
|
||||||
@@ -36,3 +37,11 @@ export type ChatChunk = Pick<
|
|||||||
> & {
|
> & {
|
||||||
streamingMessageId?: string
|
streamingMessageId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Answer = {
|
||||||
|
text: string
|
||||||
|
attachments?: {
|
||||||
|
type: string
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|||||||
6
packages/embeds/js/src/utils/toaster.ts
Normal file
6
packages/embeds/js/src/utils/toaster.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createToaster } from '@ark-ui/solid'
|
||||||
|
|
||||||
|
export const toaster = createToaster({
|
||||||
|
placement: 'bottom-end',
|
||||||
|
gap: 24,
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/nextjs",
|
"name": "@typebot.io/nextjs",
|
||||||
"version": "0.2.92",
|
"version": "0.3.0",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your Next.js website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.2.92",
|
"version": "0.3.0",
|
||||||
"description": "Convenient library to display typebots on your React app",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
9
packages/env/env.ts
vendored
9
packages/env/env.ts
vendored
@@ -341,10 +341,9 @@ const whatsAppEnv = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const upstashRedis = {
|
const redisEnv = {
|
||||||
server: {
|
server: {
|
||||||
UPSTASH_REDIS_REST_URL: z.string().url().optional(),
|
REDIS_URL: z.string().url().optional(),
|
||||||
UPSTASH_REDIS_REST_TOKEN: z.string().min(1).optional(),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,13 +424,13 @@ export const env = createEnv({
|
|||||||
...vercelEnv.server,
|
...vercelEnv.server,
|
||||||
...sleekPlanEnv.server,
|
...sleekPlanEnv.server,
|
||||||
...whatsAppEnv.server,
|
...whatsAppEnv.server,
|
||||||
...upstashRedis.server,
|
...redisEnv.server,
|
||||||
...gitlabEnv.server,
|
...gitlabEnv.server,
|
||||||
...azureEnv.server,
|
...azureEnv.server,
|
||||||
...customOAuthEnv.server,
|
...customOAuthEnv.server,
|
||||||
...sentryEnv.server,
|
...sentryEnv.server,
|
||||||
...telemetryEnv.server,
|
...telemetryEnv.server,
|
||||||
...keycloakEnv.server
|
...keycloakEnv.server,
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
...baseEnv.client,
|
...baseEnv.client,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"google-auth-library": "8.9.0",
|
"google-auth-library": "8.9.0",
|
||||||
"ky": "1.2.4",
|
"ky": "1.2.4",
|
||||||
|
"ioredis": "5.4.1",
|
||||||
"minio": "7.1.3",
|
"minio": "7.1.3",
|
||||||
"posthog-node": "3.1.1",
|
"posthog-node": "3.1.1",
|
||||||
"remark-parse": "11.0.0",
|
"remark-parse": "11.0.0",
|
||||||
|
|||||||
16
packages/lib/redis.ts
Normal file
16
packages/lib/redis.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import Redis from 'ioredis'
|
||||||
|
|
||||||
|
declare const global: { redis: Redis | undefined }
|
||||||
|
let redis: Redis | undefined
|
||||||
|
|
||||||
|
if (env.NODE_ENV === 'production' && !process.versions.bun && env.REDIS_URL) {
|
||||||
|
redis = new Redis(env.REDIS_URL)
|
||||||
|
} else if (env.REDIS_URL) {
|
||||||
|
if (!global.redis) {
|
||||||
|
global.redis = new Redis(env.REDIS_URL)
|
||||||
|
}
|
||||||
|
redis = global.redis
|
||||||
|
}
|
||||||
|
|
||||||
|
export default redis
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { SetVariableHistoryItem } from '@typebot.io/schemas/features/result'
|
import { SetVariableHistoryItem } from '@typebot.io/schemas/features/result'
|
||||||
import { isBubbleBlock, isInputBlock } from '@typebot.io/schemas/helpers'
|
import { isBubbleBlock, isInputBlock } from '@typebot.io/schemas/helpers'
|
||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
|
|
||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||||
import { createId } from '@typebot.io/lib/createId'
|
import { createId } from '@typebot.io/lib/createId'
|
||||||
import { executeCondition } from './executeCondition'
|
import { executeCondition } from './executeCondition'
|
||||||
@@ -56,7 +55,7 @@ export const computeResultTranscript = ({
|
|||||||
stopAtBlockId,
|
stopAtBlockId,
|
||||||
}: {
|
}: {
|
||||||
typebot: TypebotInSession
|
typebot: TypebotInSession
|
||||||
answers: Pick<Answer, 'blockId' | 'content'>[]
|
answers: Pick<Answer, 'blockId' | 'content' | 'attachedFileUrls'>[]
|
||||||
setVariableHistory: Pick<
|
setVariableHistory: Pick<
|
||||||
SetVariableHistoryItem,
|
SetVariableHistoryItem,
|
||||||
'blockId' | 'variableId' | 'value'
|
'blockId' | 'variableId' | 'value'
|
||||||
@@ -120,7 +119,7 @@ const executeGroup = ({
|
|||||||
typebot: TypebotInSession
|
typebot: TypebotInSession
|
||||||
resumeEdgeId?: string
|
resumeEdgeId?: string
|
||||||
}[]
|
}[]
|
||||||
answers: Pick<Answer, 'blockId' | 'content'>[]
|
answers: Pick<Answer, 'blockId' | 'content' | 'attachedFileUrls'>[]
|
||||||
setVariableHistory: Pick<
|
setVariableHistory: Pick<
|
||||||
SetVariableHistoryItem,
|
SetVariableHistoryItem,
|
||||||
'blockId' | 'variableId' | 'value'
|
'blockId' | 'variableId' | 'value'
|
||||||
@@ -157,20 +156,50 @@ const executeGroup = ({
|
|||||||
const answer = answers.shift()
|
const answer = answers.shift()
|
||||||
if (!answer) break
|
if (!answer) break
|
||||||
if (block.options?.variableId) {
|
if (block.options?.variableId) {
|
||||||
const variable = typebotsQueue[0].typebot.variables.find(
|
const replyVariable = typebotsQueue[0].typebot.variables.find(
|
||||||
(variable) => variable.id === block.options?.variableId
|
(variable) => variable.id === block.options?.variableId
|
||||||
)
|
)
|
||||||
|
if (replyVariable) {
|
||||||
|
typebotsQueue[0].typebot.variables =
|
||||||
|
typebotsQueue[0].typebot.variables.map((v) =>
|
||||||
|
v.id === replyVariable.id ? { ...v, value: answer.content } : v
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
block.type === InputBlockType.TEXT &&
|
||||||
|
block.options?.attachments?.isEnabled &&
|
||||||
|
block.options?.attachments?.saveVariableId &&
|
||||||
|
answer.attachedFileUrls &&
|
||||||
|
answer.attachedFileUrls?.length > 0
|
||||||
|
) {
|
||||||
|
const variable = typebotsQueue[0].typebot.variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === block.options?.attachments?.saveVariableId
|
||||||
|
)
|
||||||
if (variable) {
|
if (variable) {
|
||||||
typebotsQueue[0].typebot.variables =
|
typebotsQueue[0].typebot.variables =
|
||||||
typebotsQueue[0].typebot.variables.map((v) =>
|
typebotsQueue[0].typebot.variables.map((v) =>
|
||||||
v.id === variable.id ? { ...v, value: answer.content } : v
|
v.id === variable.id
|
||||||
|
? {
|
||||||
|
...v,
|
||||||
|
value: Array.isArray(variable.value)
|
||||||
|
? variable.value.concat(answer.attachedFileUrls!)
|
||||||
|
: answer.attachedFileUrls!.length === 1
|
||||||
|
? answer.attachedFileUrls![0]
|
||||||
|
: answer.attachedFileUrls,
|
||||||
|
}
|
||||||
|
: v
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentTranscript.push({
|
currentTranscript.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: answer.content,
|
text:
|
||||||
|
(answer.attachedFileUrls?.length ?? 0) > 0
|
||||||
|
? `${answer.attachedFileUrls?.join(', ')}\n\n${answer.content}`
|
||||||
|
: answer.content,
|
||||||
})
|
})
|
||||||
const outgoingEdge = getOutgoingEdgeId({
|
const outgoingEdge = getOutgoingEdgeId({
|
||||||
block,
|
block,
|
||||||
|
|||||||
@@ -320,12 +320,13 @@ model Answer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model AnswerV2 {
|
model AnswerV2 {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
blockId String
|
blockId String
|
||||||
content String @db.Text
|
content String @db.Text
|
||||||
resultId String
|
attachedFileUrls Json?
|
||||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
resultId String
|
||||||
|
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([resultId])
|
@@index([resultId])
|
||||||
@@index([blockId])
|
@@index([blockId])
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AnswerV2" ADD COLUMN "attachedFileUrls" JSONB;
|
||||||
@@ -300,12 +300,13 @@ model Answer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model AnswerV2 {
|
model AnswerV2 {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
blockId String
|
blockId String
|
||||||
content String
|
content String
|
||||||
resultId String
|
attachedFileUrls Json?
|
||||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
resultId String
|
||||||
|
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([blockId])
|
@@index([blockId])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const answerV1Schema = z.object({
|
|||||||
export const answerSchema = z.object({
|
export const answerSchema = z.object({
|
||||||
blockId: z.string(),
|
blockId: z.string(),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
|
attachedFileUrls: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const answerInputSchema = answerV1Schema
|
export const answerInputSchema = answerV1Schema
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ import { TextInputBlock } from './schema'
|
|||||||
export const defaultTextInputOptions = {
|
export const defaultTextInputOptions = {
|
||||||
isLong: false,
|
isLong: false,
|
||||||
labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' },
|
labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' },
|
||||||
|
attachments: {
|
||||||
|
isEnabled: false,
|
||||||
|
visibility: 'Auto',
|
||||||
|
},
|
||||||
} as const satisfies TextInputBlock['options']
|
} as const satisfies TextInputBlock['options']
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from '../../../../zod'
|
import { z } from '../../../../zod'
|
||||||
import { optionBaseSchema, blockBaseSchema } from '../../shared'
|
import { optionBaseSchema, blockBaseSchema } from '../../shared'
|
||||||
import { InputBlockType } from '../constants'
|
import { InputBlockType } from '../constants'
|
||||||
|
import { fileVisibilityOptions } from '../file/constants'
|
||||||
|
|
||||||
export const textInputOptionsBaseSchema = z.object({
|
export const textInputOptionsBaseSchema = z.object({
|
||||||
labels: z
|
labels: z
|
||||||
@@ -16,6 +17,13 @@ export const textInputOptionsSchema = textInputOptionsBaseSchema
|
|||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
isLong: z.boolean().optional(),
|
isLong: z.boolean().optional(),
|
||||||
|
attachments: z
|
||||||
|
.object({
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
saveVariableId: z.string().optional(),
|
||||||
|
visibility: z.enum(fileVisibilityOptions).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,23 @@ import { BubbleBlockType } from '../blocks/bubbles/constants'
|
|||||||
import { clientSideActionSchema } from './clientSideAction'
|
import { clientSideActionSchema } from './clientSideAction'
|
||||||
import { ChatSession as ChatSessionFromPrisma } from '@typebot.io/prisma'
|
import { ChatSession as ChatSessionFromPrisma } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
export const messageSchema = z.preprocess(
|
||||||
|
(val) => (typeof val === 'string' ? { type: 'text', text: val } : val),
|
||||||
|
z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('text'),
|
||||||
|
text: z.string(),
|
||||||
|
attachedFileUrls: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Can only be provided if current input block is a text input block that allows attachments'
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
export type Message = z.infer<typeof messageSchema>
|
||||||
|
|
||||||
const chatSessionSchema = z.object({
|
const chatSessionSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
@@ -183,8 +200,7 @@ export const startChatInputSchema = z.object({
|
|||||||
.describe(
|
.describe(
|
||||||
"[Where to find my bot's public ID?](../how-to#how-to-find-my-publicid)"
|
"[Where to find my bot's public ID?](../how-to#how-to-find-my-publicid)"
|
||||||
),
|
),
|
||||||
message: z
|
message: messageSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Only provide it if your flow starts with an input block and you'd like to directly provide an answer to it."
|
"Only provide it if your flow starts with an input block and you'd like to directly provide an answer to it."
|
||||||
@@ -242,7 +258,7 @@ export const startPreviewChatInputSchema = z.object({
|
|||||||
"[Where to find my bot's ID?](../how-to#how-to-find-my-typebotid)"
|
"[Where to find my bot's ID?](../how-to#how-to-find-my-typebotid)"
|
||||||
),
|
),
|
||||||
isStreamEnabled: z.boolean().optional().default(false),
|
isStreamEnabled: z.boolean().optional().default(false),
|
||||||
message: z.string().optional(),
|
message: messageSchema.optional(),
|
||||||
isOnlyRegistering: z
|
isOnlyRegistering: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -115,13 +115,13 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
|
|||||||
z.object({
|
z.object({
|
||||||
from: z.string(),
|
from: z.string(),
|
||||||
type: z.literal('image'),
|
type: z.literal('image'),
|
||||||
image: z.object({ id: z.string() }),
|
image: z.object({ id: z.string(), caption: z.string().optional() }),
|
||||||
timestamp: z.string(),
|
timestamp: z.string(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
from: z.string(),
|
from: z.string(),
|
||||||
type: z.literal('video'),
|
type: z.literal('video'),
|
||||||
video: z.object({ id: z.string() }),
|
video: z.object({ id: z.string(), caption: z.string().optional() }),
|
||||||
timestamp: z.string(),
|
timestamp: z.string(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
@@ -133,7 +133,7 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
|
|||||||
z.object({
|
z.object({
|
||||||
from: z.string(),
|
from: z.string(),
|
||||||
type: z.literal('document'),
|
type: z.literal('document'),
|
||||||
document: z.object({ id: z.string() }),
|
document: z.object({ id: z.string(), caption: z.string().optional() }),
|
||||||
timestamp: z.string(),
|
timestamp: z.string(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
784
pnpm-lock.yaml
generated
784
pnpm-lock.yaml
generated
@@ -158,9 +158,6 @@ importers:
|
|||||||
'@upstash/ratelimit':
|
'@upstash/ratelimit':
|
||||||
specifier: 0.4.3
|
specifier: 0.4.3
|
||||||
version: 0.4.3
|
version: 0.4.3
|
||||||
'@upstash/redis':
|
|
||||||
specifier: 1.22.0
|
|
||||||
version: 1.22.0
|
|
||||||
'@use-gesture/react':
|
'@use-gesture/react':
|
||||||
specifier: 10.2.27
|
specifier: 10.2.27
|
||||||
version: 10.2.27(react@18.2.0)
|
version: 10.2.27(react@18.2.0)
|
||||||
@@ -197,6 +194,9 @@ importers:
|
|||||||
immer:
|
immer:
|
||||||
specifier: 10.0.2
|
specifier: 10.0.2
|
||||||
version: 10.0.2
|
version: 10.0.2
|
||||||
|
ioredis:
|
||||||
|
specifier: ^5.4.1
|
||||||
|
version: 5.4.1
|
||||||
isolated-vm:
|
isolated-vm:
|
||||||
specifier: 4.7.2
|
specifier: 4.7.2
|
||||||
version: 4.7.2
|
version: 4.7.2
|
||||||
@@ -1001,6 +1001,9 @@ importers:
|
|||||||
|
|
||||||
packages/embeds/js:
|
packages/embeds/js:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@ark-ui/solid':
|
||||||
|
specifier: 3.3.0
|
||||||
|
version: 3.3.0(@internationalized/date@3.5.4)(solid-js@1.7.8)
|
||||||
'@stripe/stripe-js':
|
'@stripe/stripe-js':
|
||||||
specifier: 1.54.1
|
specifier: 1.54.1
|
||||||
version: 1.54.1
|
version: 1.54.1
|
||||||
@@ -1709,6 +1712,9 @@ importers:
|
|||||||
google-auth-library:
|
google-auth-library:
|
||||||
specifier: 8.9.0
|
specifier: 8.9.0
|
||||||
version: 8.9.0
|
version: 8.9.0
|
||||||
|
ioredis:
|
||||||
|
specifier: 5.4.1
|
||||||
|
version: 5.4.1
|
||||||
ky:
|
ky:
|
||||||
specifier: 1.2.4
|
specifier: 1.2.4
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
@@ -2250,6 +2256,99 @@ packages:
|
|||||||
openapi-types: 12.1.3
|
openapi-types: 12.1.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@ark-ui/anatomy@3.3.1(@internationalized/date@3.5.4):
|
||||||
|
resolution: {integrity: sha512-4cLS0C0wSthdPVH9MdOurDzgTMdq2mx1Goj86/XKPkzr6zlIydNWbNfkbyO+pd/8CyYEzHvkc3joE6NRUK/QQw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/accordion': 0.56.1
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/avatar': 0.56.1
|
||||||
|
'@zag-js/carousel': 0.56.1
|
||||||
|
'@zag-js/checkbox': 0.56.1
|
||||||
|
'@zag-js/clipboard': 0.56.1
|
||||||
|
'@zag-js/collapsible': 0.56.1
|
||||||
|
'@zag-js/color-picker': 0.56.1
|
||||||
|
'@zag-js/color-utils': 0.56.1
|
||||||
|
'@zag-js/combobox': 0.56.1
|
||||||
|
'@zag-js/date-picker': 0.56.1
|
||||||
|
'@zag-js/date-utils': 0.56.1(@internationalized/date@3.5.4)
|
||||||
|
'@zag-js/dialog': 0.56.1
|
||||||
|
'@zag-js/editable': 0.56.1
|
||||||
|
'@zag-js/file-upload': 0.56.1
|
||||||
|
'@zag-js/hover-card': 0.56.1
|
||||||
|
'@zag-js/menu': 0.56.1
|
||||||
|
'@zag-js/number-input': 0.56.1
|
||||||
|
'@zag-js/pagination': 0.56.1
|
||||||
|
'@zag-js/pin-input': 0.56.1
|
||||||
|
'@zag-js/popover': 0.56.1
|
||||||
|
'@zag-js/presence': 0.56.1
|
||||||
|
'@zag-js/progress': 0.56.1
|
||||||
|
'@zag-js/qr-code': 0.56.1
|
||||||
|
'@zag-js/radio-group': 0.56.1
|
||||||
|
'@zag-js/rating-group': 0.56.1
|
||||||
|
'@zag-js/select': 0.56.1
|
||||||
|
'@zag-js/signature-pad': 0.56.1
|
||||||
|
'@zag-js/slider': 0.56.1
|
||||||
|
'@zag-js/splitter': 0.56.1
|
||||||
|
'@zag-js/switch': 0.56.1
|
||||||
|
'@zag-js/tabs': 0.56.1
|
||||||
|
'@zag-js/tags-input': 0.56.1
|
||||||
|
'@zag-js/toast': 0.56.1
|
||||||
|
'@zag-js/toggle-group': 0.56.1
|
||||||
|
'@zag-js/tooltip': 0.56.1
|
||||||
|
'@zag-js/tree-view': 0.56.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@internationalized/date'
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@ark-ui/solid@3.3.0(@internationalized/date@3.5.4)(solid-js@1.7.8):
|
||||||
|
resolution: {integrity: sha512-jeJ2ez/TxH7aPX14uy+koeX8JN6tGjr+ZAGK2s9jeLfSMkBMvtgUeMjjm0zDRVhEPK54SlTF4wO8NPYL4I+DLw==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: '>=1.6.0'
|
||||||
|
dependencies:
|
||||||
|
'@ark-ui/anatomy': 3.3.1(@internationalized/date@3.5.4)
|
||||||
|
'@zag-js/accordion': 0.56.1
|
||||||
|
'@zag-js/avatar': 0.56.1
|
||||||
|
'@zag-js/carousel': 0.56.1
|
||||||
|
'@zag-js/checkbox': 0.56.1
|
||||||
|
'@zag-js/clipboard': 0.56.1
|
||||||
|
'@zag-js/collapsible': 0.56.1
|
||||||
|
'@zag-js/color-picker': 0.56.1
|
||||||
|
'@zag-js/combobox': 0.56.1
|
||||||
|
'@zag-js/date-picker': 0.56.1
|
||||||
|
'@zag-js/dialog': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/editable': 0.56.1
|
||||||
|
'@zag-js/file-upload': 0.56.1
|
||||||
|
'@zag-js/file-utils': 0.56.1
|
||||||
|
'@zag-js/hover-card': 0.56.1
|
||||||
|
'@zag-js/i18n-utils': 0.56.1
|
||||||
|
'@zag-js/menu': 0.56.1
|
||||||
|
'@zag-js/number-input': 0.56.1
|
||||||
|
'@zag-js/pagination': 0.56.1
|
||||||
|
'@zag-js/pin-input': 0.56.1
|
||||||
|
'@zag-js/popover': 0.56.1
|
||||||
|
'@zag-js/presence': 0.56.1
|
||||||
|
'@zag-js/progress': 0.56.1
|
||||||
|
'@zag-js/qr-code': 0.56.1
|
||||||
|
'@zag-js/radio-group': 0.56.1
|
||||||
|
'@zag-js/rating-group': 0.56.1
|
||||||
|
'@zag-js/select': 0.56.1
|
||||||
|
'@zag-js/slider': 0.56.1
|
||||||
|
'@zag-js/solid': 0.56.1(solid-js@1.7.8)
|
||||||
|
'@zag-js/splitter': 0.56.1
|
||||||
|
'@zag-js/switch': 0.56.1
|
||||||
|
'@zag-js/tabs': 0.56.1
|
||||||
|
'@zag-js/tags-input': 0.56.1
|
||||||
|
'@zag-js/toast': 0.56.1
|
||||||
|
'@zag-js/toggle-group': 0.56.1
|
||||||
|
'@zag-js/tooltip': 0.56.1
|
||||||
|
'@zag-js/tree-view': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
solid-js: 1.7.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@internationalized/date'
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@babel/code-frame@7.23.5:
|
/@babel/code-frame@7.23.5:
|
||||||
resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
|
resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -5953,6 +6052,13 @@ packages:
|
|||||||
'@floating-ui/utils': 0.2.1
|
'@floating-ui/utils': 0.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@floating-ui/dom@1.6.5:
|
||||||
|
resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.6.0
|
||||||
|
'@floating-ui/utils': 0.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@floating-ui/react-dom@1.3.0(react-dom@18.2.0)(react@18.2.0):
|
/@floating-ui/react-dom@1.3.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==}
|
resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6264,6 +6370,22 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@internationalized/date@3.5.4:
|
||||||
|
resolution: {integrity: sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==}
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@internationalized/number@3.5.3:
|
||||||
|
resolution: {integrity: sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==}
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@ioredis/commands@1.2.0:
|
||||||
|
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@isaacs/cliui@8.0.2:
|
/@isaacs/cliui@8.0.2:
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -10296,20 +10418,596 @@ packages:
|
|||||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/accordion@0.56.1:
|
||||||
|
resolution: {integrity: sha512-ylaVNTdqf5sORTUXK+9bg57fLrpA7eZkhuffPRgqw+1Di/VRCRnLjFpd0VBdzkVKvgurUF/YyewNCouVsW8TjQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/anatomy@0.56.1:
|
||||||
|
resolution: {integrity: sha512-4n7WKZ2YEYIsKSV7rEZcx6pGKhIJ6EHSn0KrC8BNI+0roJfYCfQbggih2f4E3JFT29pUFaSXIpRJa7XYyClQMw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/aria-hidden@0.56.1:
|
||||||
|
resolution: {integrity: sha512-GFUsxLZUINkBmzafUI1WlBsqld+JxECJ2+t1CFlTap3ROOoTpqGg69XnldnK+l2qEJVS5+9nAHZA5oL2491ipw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/auto-resize@0.56.1:
|
||||||
|
resolution: {integrity: sha512-MHvBL5cPeI1uNjJ7n3lap0w0m1xtxN7VcwVoLqNI44Wcf9/DmDPA+EhUQTA0koTLEI7gf+dxiRcdpnsFWpXyqQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/avatar@0.56.1:
|
||||||
|
resolution: {integrity: sha512-cUhoSuLfHZg8g/oy+eSi8cBbGL+xsKw8VBYq7dYgYB+DSuT7WMQkT9XDEa+KxHxnF0oDDKRmCcCHJTykpbzOOQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/carousel@0.56.1:
|
||||||
|
resolution: {integrity: sha512-1DnDrDxH9yUU7pA6EmGptVxNE19nLlRPHL/jpKojAYNk/D0Xsv9siJqX7+YZT1F8x4fISL8vsfjO47kHby2GfQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/checkbox@0.56.1:
|
||||||
|
resolution: {integrity: sha512-LblpuBui+1mG+pSIHySJpg0C4qgB1AM3vf3287AOa/ah9JrwqUSSLhz5Oo9/NBtnK7NDjBNS4cNX/vkDAqreuA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/clipboard@0.56.1:
|
||||||
|
resolution: {integrity: sha512-xpGf3cziVhWI/AUmR8fBb5l6TeiYvK0+rUzxbsIN6A8vZYtA+iJO7BOJxCwobC02gbLLFV4dUMve5ff+MQ8oIw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/collapsible@0.56.1:
|
||||||
|
resolution: {integrity: sha512-wJuzrgCwW+wwuWSx0GwlCyCbCDvWjFAHknkuzo3rfuKjSRl0f19QuSvfju+Ie9CcFIj/1QwN1vA/h15mmJQGqQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/collection@0.56.1:
|
||||||
|
resolution: {integrity: sha512-aA0cCQS9XeSBIg7ivJXlVFFl74ljQIeKD/TAfVoc07fLXS2cjdmR58g0xYd84twawTEYUgQYg+A3hBDFx1uPhQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/color-picker@0.56.1:
|
||||||
|
resolution: {integrity: sha512-GDAo+kUKAX5C9qvae11sML2RmsKxIGQeVkoKsUGy74BX9IzH7tEQfxejuG49Zn+4YNg/+Q+dzaar7WmTS7jqQQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/color-utils': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/text-selection': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/color-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-masYXZXn/gMbNdri/ZBk53IKvEd2TjlXpW+wzawZ5BbZjjqrCpSsvOM1lMmPvCyrJmhVQrvIZEnQXfDxfb5TSw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/numeric-range': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/combobox@0.56.1:
|
||||||
|
resolution: {integrity: sha512-NKHl2buWNE38BSU+3ZBbz9XY9ASsayO2xJQ4rU3xLEM9uUHG/Q8pP8+uaxP5x1oEaLBznzVnS/6Le7x8uRWg8A==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/aria-hidden': 0.56.1
|
||||||
|
'@zag-js/collection': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/core@0.56.1:
|
||||||
|
resolution: {integrity: sha512-3Iwum6+UmDxBqJIT4t2wSnr5hz83c6oRDS8j7I0hgV4U2Xt/IJMPedZt/KmV8ghjai/BlETskHUW1JvwZ4jaYA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/store': 0.56.1
|
||||||
|
klona: 2.0.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/date-picker@0.56.1:
|
||||||
|
resolution: {integrity: sha512-C62WGpTzQeH1IsnNVANF4JBYldXxvHpRerRhiAKA/x2bMZlyIxSre5n+O3YBSHUimDPbXBWBmqoO8PgBgtciBQ==}
|
||||||
|
dependencies:
|
||||||
|
'@internationalized/date': 3.5.4
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/date-utils': 0.56.1(@internationalized/date@3.5.4)
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/live-region': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/text-selection': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/date-utils@0.56.1(@internationalized/date@3.5.4):
|
||||||
|
resolution: {integrity: sha512-n71r6L4MV5V6YP0lwVPMsy2IV0EwnXYca+b6hSogQ2PAOwgB2JpxEBQ/F4ItPTC/yVqoEKgcQUniSTf67F+dBQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@internationalized/date': '>=3.0.0'
|
||||||
|
dependencies:
|
||||||
|
'@internationalized/date': 3.5.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/dialog@0.56.1:
|
||||||
|
resolution: {integrity: sha512-dD4A457wW5Lnz2nm9vVvDQs2H3z/CTVS6ujCq/9W61Eo3LmhCPJQOHbr1QDB8kssYYdZn6eJZsbO5PSt7gCO1w==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/aria-hidden': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/remove-scroll': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
focus-trap: 7.5.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/dismissable@0.56.1:
|
||||||
|
resolution: {integrity: sha512-jG3KMdhazYgcoW83Aay/VCKg/VParejXhKqZn+7ze5doqRXklL2LsuwSd2ecWuqX9ckB0ppd63w0IN7xAL1I8Q==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/interact-outside': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/dom-event@0.56.1:
|
||||||
|
resolution: {integrity: sha512-OUh6ELiN6RIkSFt6pQ+Ct7jdH5XoonBThBBLUKoUd5qr49bIUw+CIaJyVrngUaPSCTqpzIqS3lL3uvVbMN0W+g==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/text-selection': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@zag-js/dom-query@0.16.0:
|
/@zag-js/dom-query@0.16.0:
|
||||||
resolution: {integrity: sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==}
|
resolution: {integrity: sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/dom-query@0.56.1:
|
||||||
|
resolution: {integrity: sha512-mxUa7vzI+NhaMpf0D3cci0kmRuCPBkZCvoV+jsokkLfFKEUvAMVftnzxMxeydAEK6LFZL6PK6icOXP5FxbzzLA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/editable@0.56.1:
|
||||||
|
resolution: {integrity: sha512-zVoGRDotNwTehsLaKvPTPTMSXEkYvoUJfYd3hliYUQgo738uQTqj8TXDU8emW/WKJbHISlChiLIK05XupSMxbQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/interact-outside': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/element-rect@0.56.1:
|
||||||
|
resolution: {integrity: sha512-WPg+7sbHZnXDWczWfO1aBESQYzQhwD//yB3UtXUCSfknEdU1ZeTixLxujKgPrd/a+BlqluNbFUkI7kYRUrXsrw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@zag-js/element-size@0.10.5:
|
/@zag-js/element-size@0.10.5:
|
||||||
resolution: {integrity: sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==}
|
resolution: {integrity: sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/element-size@0.56.1:
|
||||||
|
resolution: {integrity: sha512-gwxMUkGMbQa/lB+boFxpsGI0eyOMRa0bLsz9BFrIiUTrYWMV576Le4QbFQA6UjsymlzR4WnCWEkiF5TLnCA8eQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/file-upload@0.56.1:
|
||||||
|
resolution: {integrity: sha512-g/QTQjm3ynBTPCKpYwvtwTYcbvzpWAqIjowSVtCupZVd6po+feA7aYAzfJVywR6sqQZwiLPyZR+wD2IpO7fYcg==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/file-utils': 0.56.1
|
||||||
|
'@zag-js/i18n-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/file-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-RQbvfNMTyKCBwKKzmBZcPAfk/L7HLLhr6kQvV82Dop8fKxdu4u6cdpYUwRtDn/eX4QcHJ1kQ8PpzcrVIhEggLw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/i18n-utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@zag-js/focus-visible@0.16.0:
|
/@zag-js/focus-visible@0.16.0:
|
||||||
resolution: {integrity: sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==}
|
resolution: {integrity: sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@zag-js/dom-query': 0.16.0
|
'@zag-js/dom-query': 0.16.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/form-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-7u3PjOl21muU89Ungl9bjBVqsHNNqlOnsl/2ws8t/1qkQbcGJnzAPG/AkM4njj0A5IXO9X6MqIa+9qgEYMviCQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/hover-card@0.56.1:
|
||||||
|
resolution: {integrity: sha512-cXias9qoUC2+YeZVM2Crx2WS/gio76SiKcWvg6040KvoYxNNNTOh5IqMIC+EjR9iaBdiIKPaS5HD92kGU0qbdw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/i18n-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-vLxGFsDYABn7DOYDlob4nvwr3ZaWoYJc7P51oo+xd3khmLyL2Mcq50LiH/fPKLxX+EFrnSMEjovFPHJm9rCPBQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/interact-outside@0.56.1:
|
||||||
|
resolution: {integrity: sha512-yy9Vg4vU5P0xbmJ2vKGfk09lCLM8UFECBAlLq8GNHzjj+rpi7tb4BW4AFCMcADwsilV7elB2i98r8e5/wwnbbQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/live-region@0.56.1:
|
||||||
|
resolution: {integrity: sha512-hZANI+2d079O3u+GfqWgHkngMrJbQNC0PLwLEoikhx3ruD8oXG1GX/fw+dQLpzFxvvFgkn/ABlVspmAAFuUXsw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/menu@0.56.1:
|
||||||
|
resolution: {integrity: sha512-9V8STio6PGAAoKLnRuQ4ygUCgk6NzQQ3ThMaIwwH2tCSRlam/HwP3ld9W8nnbiOrqn9RzU9XSj7ZvS567xqyzQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/rect-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/number-input@0.56.1:
|
||||||
|
resolution: {integrity: sha512-/Y5gan//XjJZKwW7EQm3tEBeDZvI4XXje5/5JVlSgZkvWNeYhlJ2xxosYR1GUvOzWV8Xp/2buaylPvNJdKtt8A==}
|
||||||
|
dependencies:
|
||||||
|
'@internationalized/number': 3.5.3
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/number-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/number-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-oz7hnTeK5NNffP3rF7K9ytkPoMSrpu15NFGDfis6oIkFx10WQn73+FqDq9c11HlkOKRUhPyYLggLgLgu0VSjjg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/numeric-range@0.56.1:
|
||||||
|
resolution: {integrity: sha512-26bVw4JMyZoO4M7Lc2wGEd2HV+cnDGN79RSjTpb2hjGcLfyxPWNDYcOjROX7CGFm3tcTq5RAvHfu6cPXYkMeng==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/pagination@0.56.1:
|
||||||
|
resolution: {integrity: sha512-trO621AuQNsrFc+DfSj0tmA8xvWO/8m1jyW1ePRQCVii1XVtlOQLUAkRUGoY2gGNL1aBr68JX3oij1ZLAlcOLw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/pin-input@0.56.1:
|
||||||
|
resolution: {integrity: sha512-8w5GvmZA/XDr5qO8e92Fpr12mjbtSv4PJPwLtWS1uTD8EXzGQbHCNxNpVbnPjva5eiX+z+RY7O/wMVr78Bk6PQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/popover@0.56.1:
|
||||||
|
resolution: {integrity: sha512-cRqjN32Kzt6xjd9E1+rF4LE4i0hzZvJQVRhahFXLsFO2r+AuK6tK4CNWZWX14BatngPm7DKI2CJei5c7UvdAeg==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/aria-hidden': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/remove-scroll': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
focus-trap: 7.5.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/popper@0.56.1:
|
||||||
|
resolution: {integrity: sha512-dAkIbVlicG78zWXezjh3qsFTtdC+nw8wkNGJv5MajDaMbhSApE/Fe1ogDbReAbMQ43DGNWpSRnU9o+DxQgoZZg==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.6.5
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/presence@0.56.1:
|
||||||
|
resolution: {integrity: sha512-x7Cb+XrZp7KWZ+3p66OQzIIa9/cVNYO/YzLovYlvdC+CcX8WykG1VtUikTbrqKGLkrjPqHksFKseIfX/BaXRHQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/progress@0.56.1:
|
||||||
|
resolution: {integrity: sha512-g/voxjIjvclT6gKblv6FueqEm2Rack46eDIDIzqPvBskS+p4QPG4y53qGZQRU6Jw5u/fKREerKCVaA2aXo4Dug==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/qr-code@0.56.1:
|
||||||
|
resolution: {integrity: sha512-OZrDqVyGr3gd4FA5DmOQU3f3J/z3WPHxmSSt/mW/pw8hNQf+xL2YpnmfpaJAk7fUJ9B+hPN24ACXYkGQVy6rIA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
proxy-memoize: 3.0.0
|
||||||
|
uqr: 0.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/radio-group@0.56.1:
|
||||||
|
resolution: {integrity: sha512-lY9Ezto4Ve0pF1u8azrypyxZ8IAZ4P5XFK9r+JKsubj00EeOFwU/5t7bseKuQtXLXuL/NDLS16GvIlVCHqePAw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/element-rect': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/rating-group@0.56.1:
|
||||||
|
resolution: {integrity: sha512-ihr/cr1e3DClrwmEKZxyk5LQPop6NFv9ElbKz4b5nQUICFCKHSAQuIuPaJbnoM4TCQWDO39manh+o7W2IhHfog==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/rect-utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-x3uUJvr2IsKk9xMdOE/IqCPxFwg6GO+s8in6cvJV0n+tePmTWfMxhIqRm/o9kftA/UZt2/LcOnK0YOkfeEBWmA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/remove-scroll@0.56.1:
|
||||||
|
resolution: {integrity: sha512-nWUEXJb2gzzcWNZiH8sydavefC79VPSBtSc1bXDlAXI3y6swY0rp/0hP20+WvCpwMI9I2o/zKTA4lY9WY3y9Kw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/select@0.56.1:
|
||||||
|
resolution: {integrity: sha512-V5EwfHGDUx4Itmuxvvlths1BTkePcs3wibywU8S3CShciLH3LlMz0EI33fgFeqjBDhtBAI8smi1jx9iT56YDVw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/collection': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/signature-pad@0.56.1:
|
||||||
|
resolution: {integrity: sha512-qOh1iUPnyZr0oAiwMzKQqGo6UgS9yFRdbTHFyN+DiRYwb9I5R71laIupETnvInLQv90wuesCBxBezNoEfZYYZw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
perfect-freehand: 1.2.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/slider@0.56.1:
|
||||||
|
resolution: {integrity: sha512-qx10xyvTIfF5eTYGrXJf+4Ix3ul6Pn//V0EtRizo141briwAzaL+sZ/hig5wqXnV1ZcfX8n0STBmls9//z35bQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/element-size': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/numeric-range': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/solid@0.56.1(solid-js@1.7.8):
|
||||||
|
resolution: {integrity: sha512-l8p618IJLZL9rVlc398zOT0uDemgpsfOEJW14aVDeb1yXQHGV9chcRX2m8YRADGAOu1jv0/HpKk6n2sUwIaYEQ==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: '>=1.1.3'
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/store': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
solid-js: 1.7.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/splitter@0.56.1:
|
||||||
|
resolution: {integrity: sha512-KkYpydjdL9hGqnsWL0atrDtUvn14lYfWXhUqH+pQwtvlc3qIZtTl5c9bLdgHcXNiN6Y/BLRqN3y9aJZzUJmYnA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/number-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/store@0.56.1:
|
||||||
|
resolution: {integrity: sha512-hc7lwqhor+WZXXXuCEV889QEHX7hBpTN0NUS+pHa8fED7QeJN+EhJZMwC3g6YLVde4w3MlxB6I92O5lruNNyEg==}
|
||||||
|
dependencies:
|
||||||
|
proxy-compare: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/switch@0.56.1:
|
||||||
|
resolution: {integrity: sha512-pvQo89TwYzjsUmgzY2ogK6JwiVwb1bzRlZ3H3pV764WfgXwZYhCq7BfcR5gLpS0Tv4jhaBCMP7Fh38SWSRePxA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/tabs@0.56.1:
|
||||||
|
resolution: {integrity: sha512-sLWeDb+PDujQ6q98staAqHDom1o4flzu8MsqgEjLoAe2hkJnYdQ/lOhmrNCu5I4+kDklBb8B9Dm0s5457EfAfA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/element-rect': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/tags-input@0.56.1:
|
||||||
|
resolution: {integrity: sha512-9merflXwcmV6ArVQQFUjWJ2WkkUeLCJll14gJj2BNf1HLUFnOX2b7Ud6e4F2Mzn5oBmM19cuoEsOLFNwCHeZzA==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/auto-resize': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/form-utils': 0.56.1
|
||||||
|
'@zag-js/interact-outside': 0.56.1
|
||||||
|
'@zag-js/live-region': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/text-selection@0.56.1:
|
||||||
|
resolution: {integrity: sha512-1r6Bz+/nLiyMw6N8opeQii6h/N7J6rNDuvys59QdbTc1rbI02DPNoRwGVfC/rBhSBK5h3vewQZePQokgnf39fQ==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/toast@0.56.1:
|
||||||
|
resolution: {integrity: sha512-giFSnmRosL8xWAVmZDBPGGO4gEjV+XrWbSybTco3x93kTdi6+qJMYw47R1kgvsBioYqSHVreGWvdlgiJOcfWNg==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dismissable': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/toggle-group@0.56.1:
|
||||||
|
resolution: {integrity: sha512-JnAuTBL7apYceD/HpaEZMa8dJCNjaXcPMcQpXC5Pm0fmPMGEG/T065QlVwJCwJE8AJ/8eF5Hzpyc889gzdxiXg==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/tooltip@0.56.1:
|
||||||
|
resolution: {integrity: sha512-0BAAdZ+Mip+EzLSs/9DYqy9JEIXxDBx8QOLRCum5biY5BpaCkCWzx/ZU9zPFK3CSr5hzoyRlJ9k/g4/UVC+mnw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/popper': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/tree-view@0.56.1:
|
||||||
|
resolution: {integrity: sha512-k3t80JH4Y9jBrX1EI/48hyyhmVx0Wz8+iHD+VCRZsQKSOCODJMDUasi5TBuGgxGsxBiu/eUquLsb4QVqsmCjmw==}
|
||||||
|
dependencies:
|
||||||
|
'@zag-js/anatomy': 0.56.1
|
||||||
|
'@zag-js/core': 0.56.1
|
||||||
|
'@zag-js/dom-event': 0.56.1
|
||||||
|
'@zag-js/dom-query': 0.56.1
|
||||||
|
'@zag-js/types': 0.56.1
|
||||||
|
'@zag-js/utils': 0.56.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/types@0.56.1:
|
||||||
|
resolution: {integrity: sha512-RkAO2PInKWo6oQmrFkwXpWO1VitL0jPs8D30r66/6+lSAEGLnCUifnkZchluJ8vlHvr04Q5emzFkIG4fQHLLmw==}
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zag-js/utils@0.56.1:
|
||||||
|
resolution: {integrity: sha512-zJ1HxV+26I6Uu0M112ybEJUsHXlHQ2bE9XuDX+n7uDzPbilMvLQ87XYhJ40bb2lykgNsKCWhLWV9xkiEVJD38w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@zxing/text-encoding@0.9.0:
|
/@zxing/text-encoding@0.9.0:
|
||||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@@ -11535,6 +12233,11 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cluster-key-slot@1.1.2:
|
||||||
|
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/co-body@6.1.0:
|
/co-body@6.1.0:
|
||||||
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
|
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12246,6 +12949,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
/denque@2.1.0:
|
||||||
|
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/depd@1.1.2:
|
/depd@1.1.2:
|
||||||
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -13754,6 +14462,12 @@ packages:
|
|||||||
tslib: 2.6.0
|
tslib: 2.6.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/focus-trap@7.5.4:
|
||||||
|
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
|
||||||
|
dependencies:
|
||||||
|
tabbable: 6.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/focus-visible@5.2.0:
|
/focus-visible@5.2.0:
|
||||||
resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==}
|
resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -14766,6 +15480,23 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ioredis@5.4.1:
|
||||||
|
resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
|
||||||
|
engines: {node: '>=12.22.0'}
|
||||||
|
dependencies:
|
||||||
|
'@ioredis/commands': 1.2.0
|
||||||
|
cluster-key-slot: 1.1.2
|
||||||
|
debug: 4.3.4
|
||||||
|
denque: 2.1.0
|
||||||
|
lodash.defaults: 4.2.0
|
||||||
|
lodash.isarguments: 3.1.0
|
||||||
|
redis-errors: 1.2.0
|
||||||
|
redis-parser: 3.0.0
|
||||||
|
standard-as-callback: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ip-regex@4.3.0:
|
/ip-regex@4.3.0:
|
||||||
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
|
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -15930,6 +16661,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/klona@2.0.6:
|
||||||
|
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/known-css-properties@0.24.0:
|
/known-css-properties@0.24.0:
|
||||||
resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==}
|
resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -16045,6 +16781,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.defaults@4.2.0:
|
||||||
|
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.includes@4.3.0:
|
/lodash.includes@4.3.0:
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -16053,6 +16793,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SC4Usc0XbIKuz3eH7oNwPqibKHfTJSGVZwO/6eGhdoPzqexOY7z43pKo8xz0M5zzXSRteADV6fW7cRf6Ru0+VA==}
|
resolution: {integrity: sha512-SC4Usc0XbIKuz3eH7oNwPqibKHfTJSGVZwO/6eGhdoPzqexOY7z43pKo8xz0M5zzXSRteADV6fW7cRf6Ru0+VA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.isarguments@3.1.0:
|
||||||
|
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.isfunction@3.0.9:
|
/lodash.isfunction@3.0.9:
|
||||||
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
|
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -18302,6 +19046,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/perfect-freehand@1.2.2:
|
||||||
|
resolution: {integrity: sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/periscopic@3.1.0:
|
/periscopic@3.1.0:
|
||||||
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18974,9 +19722,19 @@ packages:
|
|||||||
/proxy-compare@2.6.0:
|
/proxy-compare@2.6.0:
|
||||||
resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==}
|
resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==}
|
||||||
|
|
||||||
|
/proxy-compare@3.0.0:
|
||||||
|
resolution: {integrity: sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/proxy-from-env@1.1.0:
|
/proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
|
/proxy-memoize@3.0.0:
|
||||||
|
resolution: {integrity: sha512-2fs4eIg4w6SfOjKHGVdg5tJ9WgHifEXKo2gfS/+tHGajO2YtAu03lLs+ltNKnteGKvq3SvHromkZeKus4J39/g==}
|
||||||
|
dependencies:
|
||||||
|
proxy-compare: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pseudomap@1.0.2:
|
/pseudomap@1.0.2:
|
||||||
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
|
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -19520,6 +20278,18 @@ packages:
|
|||||||
strip-indent: 3.0.0
|
strip-indent: 3.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/redis-errors@1.2.0:
|
||||||
|
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/redis-parser@3.0.0:
|
||||||
|
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
dependencies:
|
||||||
|
redis-errors: 1.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/reflect.getprototypeof@1.0.5:
|
/reflect.getprototypeof@1.0.5:
|
||||||
resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==}
|
resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -20486,6 +21256,10 @@ packages:
|
|||||||
type-fest: 0.7.1
|
type-fest: 0.7.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/standard-as-callback@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/statuses@1.5.0:
|
/statuses@1.5.0:
|
||||||
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -21710,6 +22484,10 @@ packages:
|
|||||||
/upper-case@1.1.3:
|
/upper-case@1.1.3:
|
||||||
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
|
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
|
||||||
|
|
||||||
|
/uqr@0.1.2:
|
||||||
|
resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/uri-js@4.4.1:
|
/uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user