@ -121,6 +121,7 @@ const UploadFileContent = ({
|
|||||||
}: ContentProps & { filePath: string; includeFileName?: boolean }) => (
|
}: ContentProps & { filePath: string; includeFileName?: boolean }) => (
|
||||||
<Flex justify="center" py="2">
|
<Flex justify="center" py="2">
|
||||||
<UploadButton
|
<UploadButton
|
||||||
|
fileType="image"
|
||||||
filePath={filePath}
|
filePath={filePath}
|
||||||
onFileUploaded={onNewUrl}
|
onFileUploaded={onNewUrl}
|
||||||
includeFileName={includeFileName}
|
includeFileName={includeFileName}
|
||||||
|
@ -4,12 +4,14 @@ import { ChangeEvent, useState } from 'react'
|
|||||||
import { uploadFiles } from 'utils'
|
import { uploadFiles } from 'utils'
|
||||||
|
|
||||||
type UploadButtonProps = {
|
type UploadButtonProps = {
|
||||||
|
fileType: 'image' | 'audio'
|
||||||
filePath: string
|
filePath: string
|
||||||
includeFileName?: boolean
|
includeFileName?: boolean
|
||||||
onFileUploaded: (url: string) => void
|
onFileUploaded: (url: string) => void
|
||||||
} & ButtonProps
|
} & ButtonProps
|
||||||
|
|
||||||
export const UploadButton = ({
|
export const UploadButton = ({
|
||||||
|
fileType,
|
||||||
filePath,
|
filePath,
|
||||||
includeFileName,
|
includeFileName,
|
||||||
onFileUploaded,
|
onFileUploaded,
|
||||||
@ -41,7 +43,11 @@ export const UploadButton = ({
|
|||||||
id="file-input"
|
id="file-input"
|
||||||
display="none"
|
display="none"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
accept=".jpg, .jpeg, .png, .svg, .gif"
|
accept={
|
||||||
|
fileType === 'image'
|
||||||
|
? '.jpg, .jpeg, .png, .svg, .gif'
|
||||||
|
: '.mp3, .wav, .ogg'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="label"
|
as="label"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { IconProps, Icon } from '@chakra-ui/react'
|
import { IconProps, Icon } from '@chakra-ui/react'
|
||||||
|
|
||||||
const featherIconsBaseProps: IconProps = {
|
export const featherIconsBaseProps: IconProps = {
|
||||||
fill: 'none',
|
fill: 'none',
|
||||||
stroke: 'currentColor',
|
stroke: 'currentColor',
|
||||||
strokeWidth: '2px',
|
strokeWidth: '2px',
|
||||||
|
@ -52,6 +52,7 @@ export const MyAccountForm = () => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<UploadButton
|
<UploadButton
|
||||||
size="sm"
|
size="sm"
|
||||||
|
fileType="image"
|
||||||
filePath={`users/${user?.id}/avatar`}
|
filePath={`users/${user?.id}/avatar`}
|
||||||
leftIcon={<UploadIcon />}
|
leftIcon={<UploadIcon />}
|
||||||
onFileUploaded={handleFileUploaded}
|
onFileUploaded={handleFileUploaded}
|
||||||
|
41
apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
Normal file
41
apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||||
|
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||||
|
import { BubbleBlockType, defaultAudioBubbleContent } from 'models'
|
||||||
|
import cuid from 'cuid'
|
||||||
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||||
|
|
||||||
|
const audioSampleUrl =
|
||||||
|
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
||||||
|
|
||||||
|
test('should work as expected', async ({ page }) => {
|
||||||
|
const typebotId = cuid()
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
...parseDefaultGroupWithBlock({
|
||||||
|
type: BubbleBlockType.AUDIO,
|
||||||
|
content: defaultAudioBubbleContent,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.getByText('Click to edit...').click()
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Paste the audio file link...')
|
||||||
|
.fill(audioSampleUrl)
|
||||||
|
await expect(page.locator('audio')).toHaveAttribute('src', audioSampleUrl)
|
||||||
|
await page.getByRole('button', { name: 'Upload' }).click()
|
||||||
|
await page.setInputFiles('input[type="file"]', getTestAsset('sample.mp3'))
|
||||||
|
await expect(page.locator('audio')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
|
||||||
|
)
|
||||||
|
await page.getByRole('button', { name: 'Preview' }).click()
|
||||||
|
await expect(typebotViewer(page).locator('audio')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
|
||||||
|
)
|
||||||
|
})
|
@ -0,0 +1,68 @@
|
|||||||
|
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
|
||||||
|
import { AudioBubbleContent } from 'models'
|
||||||
|
import { Input } from '@/components/inputs'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fileUploadPath: string
|
||||||
|
content: AudioBubbleContent
|
||||||
|
onSubmit: (content: AudioBubbleContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AudioBubbleForm = ({
|
||||||
|
fileUploadPath,
|
||||||
|
content,
|
||||||
|
onSubmit,
|
||||||
|
}: Props) => {
|
||||||
|
const [currentTab, setCurrentTab] = useState<'link' | 'upload'>('link')
|
||||||
|
|
||||||
|
const submit = (url: string) => onSubmit({ url })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<HStack>
|
||||||
|
<Button
|
||||||
|
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||||
|
onClick={() => setCurrentTab('upload')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
||||||
|
onClick={() => setCurrentTab('link')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Embed link
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
<Stack p="2">
|
||||||
|
{currentTab === 'upload' && (
|
||||||
|
<Flex justify="center" py="2">
|
||||||
|
<UploadButton
|
||||||
|
fileType="audio"
|
||||||
|
filePath={fileUploadPath}
|
||||||
|
onFileUploaded={submit}
|
||||||
|
colorScheme="blue"
|
||||||
|
>
|
||||||
|
Choose a file
|
||||||
|
</UploadButton>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{currentTab === 'link' && (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
placeholder="Paste the audio file link..."
|
||||||
|
defaultValue={content.url ?? ''}
|
||||||
|
onChange={submit}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||||
|
Works with .MP3s, .WAVs and .OGGs
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { featherIconsBaseProps } from '@/components/icons'
|
||||||
|
import { Icon, IconProps } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const AudioBubbleIcon = (props: IconProps) => (
|
||||||
|
<Icon color="blue.500" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path>
|
||||||
|
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import { AudioBubbleContent } from 'models'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: AudioBubbleContent['url']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AudioBubbleNode = ({ url }: Props) =>
|
||||||
|
isDefined(url) ? (
|
||||||
|
<audio src={url} controls />
|
||||||
|
) : (
|
||||||
|
<Text color={'gray.500'}>Click to edit...</Text>
|
||||||
|
)
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './AudioBubbleNode'
|
||||||
|
export * from './AudioBubbleIcon'
|
1
apps/builder/src/features/blocks/bubbles/audio/index.ts
Normal file
1
apps/builder/src/features/blocks/bubbles/audio/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './components'
|
@ -35,6 +35,7 @@ import { NumberInputIcon } from '@/features/blocks/inputs/number'
|
|||||||
import { TextInputIcon } from '@/features/blocks/inputs/textInput'
|
import { TextInputIcon } from '@/features/blocks/inputs/textInput'
|
||||||
import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed'
|
import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed'
|
||||||
import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalytics'
|
import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalytics'
|
||||||
|
import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio'
|
||||||
|
|
||||||
type BlockIconProps = { type: BlockType } & IconProps
|
type BlockIconProps = { type: BlockType } & IconProps
|
||||||
|
|
||||||
@ -47,37 +48,39 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
|
|||||||
case BubbleBlockType.VIDEO:
|
case BubbleBlockType.VIDEO:
|
||||||
return <VideoBubbleIcon {...props} />
|
return <VideoBubbleIcon {...props} />
|
||||||
case BubbleBlockType.EMBED:
|
case BubbleBlockType.EMBED:
|
||||||
return <EmbedBubbleIcon color="blue.500" {...props} />
|
return <EmbedBubbleIcon {...props} />
|
||||||
|
case BubbleBlockType.AUDIO:
|
||||||
|
return <AudioBubbleIcon {...props} />
|
||||||
case InputBlockType.TEXT:
|
case InputBlockType.TEXT:
|
||||||
return <TextInputIcon color="orange.500" {...props} />
|
return <TextInputIcon {...props} />
|
||||||
case InputBlockType.NUMBER:
|
case InputBlockType.NUMBER:
|
||||||
return <NumberInputIcon color="orange.500" {...props} />
|
return <NumberInputIcon {...props} />
|
||||||
case InputBlockType.EMAIL:
|
case InputBlockType.EMAIL:
|
||||||
return <EmailInputIcon color="orange.500" {...props} />
|
return <EmailInputIcon {...props} />
|
||||||
case InputBlockType.URL:
|
case InputBlockType.URL:
|
||||||
return <UrlInputIcon color="orange.500" {...props} />
|
return <UrlInputIcon {...props} />
|
||||||
case InputBlockType.DATE:
|
case InputBlockType.DATE:
|
||||||
return <DateInputIcon color="orange.500" {...props} />
|
return <DateInputIcon {...props} />
|
||||||
case InputBlockType.PHONE:
|
case InputBlockType.PHONE:
|
||||||
return <PhoneInputIcon color="orange.500" {...props} />
|
return <PhoneInputIcon {...props} />
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
return <ButtonsInputIcon color="orange.500" {...props} />
|
return <ButtonsInputIcon {...props} />
|
||||||
case InputBlockType.PAYMENT:
|
case InputBlockType.PAYMENT:
|
||||||
return <PaymentInputIcon color="orange.500" {...props} />
|
return <PaymentInputIcon {...props} />
|
||||||
case InputBlockType.RATING:
|
case InputBlockType.RATING:
|
||||||
return <RatingInputIcon color="orange.500" {...props} />
|
return <RatingInputIcon {...props} />
|
||||||
case InputBlockType.FILE:
|
case InputBlockType.FILE:
|
||||||
return <FileInputIcon color="orange.500" {...props} />
|
return <FileInputIcon {...props} />
|
||||||
case LogicBlockType.SET_VARIABLE:
|
case LogicBlockType.SET_VARIABLE:
|
||||||
return <SetVariableIcon color="purple.500" {...props} />
|
return <SetVariableIcon {...props} />
|
||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
return <ConditionIcon color="purple.500" {...props} />
|
return <ConditionIcon {...props} />
|
||||||
case LogicBlockType.REDIRECT:
|
case LogicBlockType.REDIRECT:
|
||||||
return <RedirectIcon color="purple.500" {...props} />
|
return <RedirectIcon {...props} />
|
||||||
case LogicBlockType.CODE:
|
case LogicBlockType.CODE:
|
||||||
return <CodeIcon color="purple.500" {...props} />
|
return <CodeIcon {...props} />
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return <TypebotLinkIcon color="purple.500" {...props} />
|
return <TypebotLinkIcon {...props} />
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
return <GoogleSheetsLogo {...props} />
|
return <GoogleSheetsLogo {...props} />
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
|
@ -32,6 +32,8 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
|
|||||||
<Text>Embed</Text>
|
<Text>Embed</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
case BubbleBlockType.AUDIO:
|
||||||
|
return <Text>Audio</Text>
|
||||||
case InputBlockType.NUMBER:
|
case InputBlockType.NUMBER:
|
||||||
return <Text>Number</Text>
|
return <Text>Number</Text>
|
||||||
case InputBlockType.EMAIL:
|
case InputBlockType.EMAIL:
|
||||||
|
@ -36,6 +36,7 @@ import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
|||||||
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
||||||
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
|
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
|
||||||
import { MakeComNodeContent } from '@/features/blocks/integrations/makeCom'
|
import { MakeComNodeContent } from '@/features/blocks/integrations/makeCom'
|
||||||
|
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: Block | StartBlock
|
block: Block | StartBlock
|
||||||
@ -66,6 +67,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
case BubbleBlockType.EMBED: {
|
case BubbleBlockType.EMBED: {
|
||||||
return <EmbedBubbleContent block={block} />
|
return <EmbedBubbleContent block={block} />
|
||||||
}
|
}
|
||||||
|
case BubbleBlockType.AUDIO: {
|
||||||
|
return <AudioBubbleNode url={block.content.url} />
|
||||||
|
}
|
||||||
case InputBlockType.TEXT: {
|
case InputBlockType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<TextInputNodeContent
|
<TextInputNodeContent
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
|
import { AudioBubbleForm } from '@/features/blocks/bubbles/audio/components/AudioBubbleForm'
|
||||||
import { EmbedUploadContent } from '@/features/blocks/bubbles/embed'
|
import { EmbedUploadContent } from '@/features/blocks/bubbles/embed'
|
||||||
import { VideoUploadContent } from '@/features/blocks/bubbles/video'
|
import { VideoUploadContent } from '@/features/blocks/bubbles/video'
|
||||||
import {
|
import {
|
||||||
@ -73,5 +74,14 @@ export const MediaBubbleContent = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case BubbleBlockType.AUDIO: {
|
||||||
|
return (
|
||||||
|
<AudioBubbleForm
|
||||||
|
content={block.content}
|
||||||
|
fileUploadPath={`typebots/${typebotId}/blocks/${block.id}`}
|
||||||
|
onSubmit={onContentChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
defaultGoogleAnalyticsOptions,
|
defaultGoogleAnalyticsOptions,
|
||||||
defaultGoogleSheetsOptions,
|
defaultGoogleSheetsOptions,
|
||||||
defaultImageBubbleContent,
|
defaultImageBubbleContent,
|
||||||
|
defaultAudioBubbleContent,
|
||||||
defaultNumberInputOptions,
|
defaultNumberInputOptions,
|
||||||
defaultPaymentInputOptions,
|
defaultPaymentInputOptions,
|
||||||
defaultPhoneInputOptions,
|
defaultPhoneInputOptions,
|
||||||
@ -400,6 +401,8 @@ const parseDefaultContent = (type: BubbleBlockType): BubbleBlockContent => {
|
|||||||
return defaultVideoBubbleContent
|
return defaultVideoBubbleContent
|
||||||
case BubbleBlockType.EMBED:
|
case BubbleBlockType.EMBED:
|
||||||
return defaultEmbedBubbleContent
|
return defaultEmbedBubbleContent
|
||||||
|
case BubbleBlockType.AUDIO:
|
||||||
|
return defaultAudioBubbleContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
apps/builder/src/test/assets/sample.mp3
Normal file
BIN
apps/builder/src/test/assets/sample.mp3
Normal file
Binary file not shown.
21
apps/docs/docs/editor/blocks/bubbles/audio.mdx
Normal file
21
apps/docs/docs/editor/blocks/bubbles/audio.mdx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { FlowToBot } from '../../../../src/js/FlowToBot'
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
|
||||||
|
The Audio bubble block allows you to play a recorded audio to your user. You can upload an audio file or directly paste a URL.
|
||||||
|
|
||||||
|
<FlowToBot
|
||||||
|
flow={
|
||||||
|
<img
|
||||||
|
src="/img/blocks/bubbles/audio/editor.png"
|
||||||
|
width="100%"
|
||||||
|
style={{ maxWidth: '400px' }}
|
||||||
|
alt="Audio bubble"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
bot={
|
||||||
|
<video controls width="100%" style={{ maxWidth: '400px' }}>
|
||||||
|
<source src="/img/blocks/bubbles/audio/bot.mp4" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
}
|
||||||
|
/>
|
@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
sidebar_position: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
import { FlowToBot } from '../../../../src/js/FlowToBot'
|
import { FlowToBot } from '../../../../src/js/FlowToBot'
|
||||||
|
|
||||||
# Embed
|
# Embed
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
sidebar_position: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
import { FlowToBot } from '../../../../src/js/FlowToBot'
|
import { FlowToBot } from '../../../../src/js/FlowToBot'
|
||||||
|
|
||||||
# Video
|
# Video
|
||||||
|
BIN
apps/docs/static/img/blocks/bubbles/audio/bot.mp4
vendored
Normal file
BIN
apps/docs/static/img/blocks/bubbles/audio/bot.mp4
vendored
Normal file
Binary file not shown.
BIN
apps/docs/static/img/blocks/bubbles/audio/editor.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/bubbles/audio/editor.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
import { AudioBubble } from '@/features/blocks/bubbles/audio'
|
||||||
import { EmbedBubble } from '@/features/blocks/bubbles/embed'
|
import { EmbedBubble } from '@/features/blocks/bubbles/embed'
|
||||||
import { ImageBubble } from '@/features/blocks/bubbles/image'
|
import { ImageBubble } from '@/features/blocks/bubbles/image'
|
||||||
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
|
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
|
||||||
@ -20,5 +21,12 @@ export const HostBubble = ({ block, onTransitionEnd }: Props) => {
|
|||||||
return <VideoBubble block={block} onTransitionEnd={onTransitionEnd} />
|
return <VideoBubble block={block} onTransitionEnd={onTransitionEnd} />
|
||||||
case BubbleBlockType.EMBED:
|
case BubbleBlockType.EMBED:
|
||||||
return <EmbedBubble block={block} onTransitionEnd={onTransitionEnd} />
|
return <EmbedBubble block={block} onTransitionEnd={onTransitionEnd} />
|
||||||
|
case BubbleBlockType.AUDIO:
|
||||||
|
return (
|
||||||
|
<AudioBubble
|
||||||
|
url={block.content.url}
|
||||||
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTypebot } from '@/providers/TypebotProvider'
|
||||||
|
import { AudioBubbleContent } from 'models'
|
||||||
|
import { TypingBubble } from '@/components/TypingBubble'
|
||||||
|
import { parseVariables } from '@/features/variables'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: AudioBubbleContent['url']
|
||||||
|
onTransitionEnd: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAnimationDuration = 400
|
||||||
|
const typingDuration = 500
|
||||||
|
|
||||||
|
export const AudioBubble = ({ url, onTransitionEnd }: Props) => {
|
||||||
|
const { typebot, isLoading } = useTypebot()
|
||||||
|
const audio = useRef<HTMLAudioElement | null>(null)
|
||||||
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
|
const parsedUrl = useMemo(
|
||||||
|
() => parseVariables(typebot.variables)(url),
|
||||||
|
[url, typebot.variables]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTyping || isLoading) return
|
||||||
|
|
||||||
|
const typingTimeout = setTimeout(() => {
|
||||||
|
setIsTyping(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
onTransitionEnd()
|
||||||
|
}, showAnimationDuration)
|
||||||
|
}, typingDuration)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(typingTimeout)
|
||||||
|
}
|
||||||
|
}, [isLoading, isTyping, onTransitionEnd])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||||
|
<div className={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||||
|
<div
|
||||||
|
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing z-10 "
|
||||||
|
style={{
|
||||||
|
width: isTyping ? '4rem' : '100%',
|
||||||
|
height: isTyping ? '2rem' : '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTyping ? <TypingBubble /> : null}
|
||||||
|
</div>
|
||||||
|
<audio
|
||||||
|
ref={audio}
|
||||||
|
src={parsedUrl}
|
||||||
|
className={
|
||||||
|
'z-10 content-opacity m-2 ' +
|
||||||
|
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||||
|
}
|
||||||
|
style={{ height: isTyping ? '2rem' : 'revert' }}
|
||||||
|
autoPlay
|
||||||
|
controls
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './AudioBubble'
|
@ -0,0 +1 @@
|
|||||||
|
export * from './components'
|
18
packages/models/src/features/blocks/bubbles/audio.ts
Normal file
18
packages/models/src/features/blocks/bubbles/audio.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { blockBaseSchema, BubbleBlockType } from '../shared'
|
||||||
|
|
||||||
|
export const audioBubbleContentSchema = z.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const audioBubbleBlockSchema = blockBaseSchema.and(
|
||||||
|
z.object({
|
||||||
|
type: z.enum([BubbleBlockType.AUDIO]),
|
||||||
|
content: audioBubbleContentSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultAudioBubbleContent = {}
|
||||||
|
|
||||||
|
export type AudioBubbleBlock = z.infer<typeof audioBubbleBlockSchema>
|
||||||
|
export type AudioBubbleContent = z.infer<typeof audioBubbleContentSchema>
|
@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { audioBubbleBlockSchema, audioBubbleContentSchema } from './audio'
|
||||||
import { embedBubbleContentSchema, embedBubbleBlockSchema } from './embed'
|
import { embedBubbleContentSchema, embedBubbleBlockSchema } from './embed'
|
||||||
import { imageBubbleContentSchema, imageBubbleBlockSchema } from './image'
|
import { imageBubbleContentSchema, imageBubbleBlockSchema } from './image'
|
||||||
import { textBubbleContentSchema, textBubbleBlockSchema } from './text'
|
import { textBubbleContentSchema, textBubbleBlockSchema } from './text'
|
||||||
@ -8,11 +9,13 @@ export const bubbleBlockContentSchema = textBubbleContentSchema
|
|||||||
.or(imageBubbleContentSchema)
|
.or(imageBubbleContentSchema)
|
||||||
.or(videoBubbleContentSchema)
|
.or(videoBubbleContentSchema)
|
||||||
.or(embedBubbleContentSchema)
|
.or(embedBubbleContentSchema)
|
||||||
|
.or(audioBubbleContentSchema)
|
||||||
|
|
||||||
export const bubbleBlockSchema = textBubbleBlockSchema
|
export const bubbleBlockSchema = textBubbleBlockSchema
|
||||||
.or(imageBubbleBlockSchema)
|
.or(imageBubbleBlockSchema)
|
||||||
.or(videoBubbleBlockSchema)
|
.or(videoBubbleBlockSchema)
|
||||||
.or(embedBubbleBlockSchema)
|
.or(embedBubbleBlockSchema)
|
||||||
|
.or(audioBubbleBlockSchema)
|
||||||
|
|
||||||
export type BubbleBlock = z.infer<typeof bubbleBlockSchema>
|
export type BubbleBlock = z.infer<typeof bubbleBlockSchema>
|
||||||
export type BubbleBlockContent = z.infer<typeof bubbleBlockContentSchema>
|
export type BubbleBlockContent = z.infer<typeof bubbleBlockContentSchema>
|
||||||
|
@ -3,3 +3,4 @@ export * from './text'
|
|||||||
export * from './image'
|
export * from './image'
|
||||||
export * from './video'
|
export * from './video'
|
||||||
export * from './embed'
|
export * from './embed'
|
||||||
|
export * from './audio'
|
||||||
|
@ -28,6 +28,7 @@ export enum BubbleBlockType {
|
|||||||
IMAGE = 'image',
|
IMAGE = 'image',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
EMBED = 'embed',
|
EMBED = 'embed',
|
||||||
|
AUDIO = 'audio',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InputBlockType {
|
export enum InputBlockType {
|
||||||
|
Reference in New Issue
Block a user