139
apps/builder/src/components/Toast.tsx
Normal file
139
apps/builder/src/components/Toast.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Flex, HStack, IconButton, Stack, Text } from '@chakra-ui/react'
|
||||||
|
import { AlertIcon, CloseIcon, InfoIcon, SmileIcon } from './icons'
|
||||||
|
import { CodeEditor } from './inputs/CodeEditor'
|
||||||
|
import { LanguageName } from '@uiw/codemirror-extensions-langs'
|
||||||
|
|
||||||
|
export type ToastProps = {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
details?: {
|
||||||
|
content: string
|
||||||
|
lang: LanguageName
|
||||||
|
}
|
||||||
|
status?: 'info' | 'error' | 'success'
|
||||||
|
icon?: React.ReactNode
|
||||||
|
primaryButton?: React.ReactNode
|
||||||
|
secondaryButton?: React.ReactNode
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toast = ({
|
||||||
|
status = 'error',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
icon,
|
||||||
|
primaryButton,
|
||||||
|
secondaryButton,
|
||||||
|
onClose,
|
||||||
|
}: ToastProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
p={3}
|
||||||
|
rounded="md"
|
||||||
|
bgColor="white"
|
||||||
|
borderWidth="1px"
|
||||||
|
shadow="sm"
|
||||||
|
fontSize="sm"
|
||||||
|
pos="relative"
|
||||||
|
maxW="450px"
|
||||||
|
>
|
||||||
|
<HStack alignItems="flex-start" pr="7" spacing="3" w="full">
|
||||||
|
<Icon customIcon={icon} status={status} />{' '}
|
||||||
|
<Stack spacing={3} flex="1">
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{title && <Text fontWeight="semibold">{title}</Text>}
|
||||||
|
{description && <Text>{description}</Text>}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<CodeEditor
|
||||||
|
isReadOnly
|
||||||
|
value={details.content}
|
||||||
|
lang={details.lang}
|
||||||
|
maxHeight="200px"
|
||||||
|
maxWidth="calc(450px - 100px)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(secondaryButton || primaryButton) && (
|
||||||
|
<HStack>
|
||||||
|
{secondaryButton}
|
||||||
|
{primaryButton}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
aria-label="Close"
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
variant="ghost"
|
||||||
|
pos="absolute"
|
||||||
|
top={1}
|
||||||
|
right={1}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = ({
|
||||||
|
customIcon,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
customIcon?: React.ReactNode
|
||||||
|
status: ToastProps['status']
|
||||||
|
}) => {
|
||||||
|
const color = parseColor(status)
|
||||||
|
const icon = parseIcon(status, customIcon)
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
bgColor={`${color}.50`}
|
||||||
|
boxSize="40px"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
rounded="full"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
bgColor={`${color}.100`}
|
||||||
|
boxSize="30px"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
rounded="full"
|
||||||
|
fontSize="18px"
|
||||||
|
color={`${color}.600`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseColor = (status: ToastProps['status']) => {
|
||||||
|
if (!status) return 'red'
|
||||||
|
switch (status) {
|
||||||
|
case 'error':
|
||||||
|
return 'red'
|
||||||
|
case 'success':
|
||||||
|
return 'green'
|
||||||
|
case 'info':
|
||||||
|
return 'blue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseIcon = (
|
||||||
|
status: ToastProps['status'],
|
||||||
|
customIcon?: React.ReactNode
|
||||||
|
) => {
|
||||||
|
if (customIcon) return customIcon
|
||||||
|
switch (status) {
|
||||||
|
case 'error':
|
||||||
|
return <AlertIcon />
|
||||||
|
case 'success':
|
||||||
|
return <SmileIcon />
|
||||||
|
case 'info':
|
||||||
|
return <InfoIcon />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -604,3 +604,20 @@ export const ShuffleIcon = (props: IconProps) => (
|
|||||||
<line x1="4" y1="4" x2="9" y2="9"></line>
|
<line x1="4" y1="4" x2="9" y2="9"></line>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const InfoIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SmileIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
||||||
|
<line x1="9" y1="9" x2="9.01" y2="9"></line>
|
||||||
|
<line x1="15" y1="9" x2="15.01" y2="9"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Props = {
|
|||||||
debounceTimeout?: number
|
debounceTimeout?: number
|
||||||
withVariableButton?: boolean
|
withVariableButton?: boolean
|
||||||
height?: string
|
height?: string
|
||||||
|
maxHeight?: string
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
}
|
}
|
||||||
export const CodeEditor = ({
|
export const CodeEditor = ({
|
||||||
@@ -32,6 +33,7 @@ export const CodeEditor = ({
|
|||||||
lang,
|
lang,
|
||||||
onChange,
|
onChange,
|
||||||
height = '250px',
|
height = '250px',
|
||||||
|
maxHeight = '70vh',
|
||||||
withVariableButton = true,
|
withVariableButton = true,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
debounceTimeout = 1000,
|
debounceTimeout = 1000,
|
||||||
@@ -93,9 +95,10 @@ export const CodeEditor = ({
|
|||||||
pos="relative"
|
pos="relative"
|
||||||
onMouseEnter={onOpen}
|
onMouseEnter={onOpen}
|
||||||
onMouseLeave={onClose}
|
onMouseLeave={onClose}
|
||||||
|
maxWidth={props.maxWidth}
|
||||||
sx={{
|
sx={{
|
||||||
'& .cm-editor': {
|
'& .cm-editor': {
|
||||||
maxH: '70vh',
|
maxH: maxHeight,
|
||||||
outline: '0px solid transparent !important',
|
outline: '0px solid transparent !important',
|
||||||
rounded: 'md',
|
rounded: 'md',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { WebhookIcon } from '@/components/icons'
|
||||||
import { useEditor } from '@/features/editor/providers/EditorProvider'
|
import { useEditor } from '@/features/editor/providers/EditorProvider'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { UseToastOptions } from '@chakra-ui/react'
|
|
||||||
import { Standard } from '@typebot.io/react'
|
import { Standard } from '@typebot.io/react'
|
||||||
import { ChatReply } from '@typebot.io/schemas'
|
import { ChatReply } from '@typebot.io/schemas'
|
||||||
|
|
||||||
@@ -15,7 +15,17 @@ export const WebPreview = () => {
|
|||||||
|
|
||||||
const handleNewLogs = (logs: ChatReply['logs']) => {
|
const handleNewLogs = (logs: ChatReply['logs']) => {
|
||||||
logs?.forEach((log) => {
|
logs?.forEach((log) => {
|
||||||
showToast(log as UseToastOptions)
|
showToast({
|
||||||
|
icon: <WebhookIcon />,
|
||||||
|
title: 'An error occured',
|
||||||
|
description: log.description,
|
||||||
|
details: log.details
|
||||||
|
? {
|
||||||
|
lang: 'json',
|
||||||
|
content: JSON.stringify(log.details, null, 2),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
console.error(log)
|
console.error(log)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useToast as useChakraToast, UseToastOptions } from '@chakra-ui/react'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
|
|
||||||
export const useToast = () => {
|
|
||||||
const toast = useChakraToast()
|
|
||||||
|
|
||||||
const showToast = useCallback(
|
|
||||||
({ title, description, status = 'error', ...props }: UseToastOptions) => {
|
|
||||||
toast({
|
|
||||||
position: 'top-right',
|
|
||||||
description,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
isClosable: true,
|
|
||||||
...props,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[toast]
|
|
||||||
)
|
|
||||||
|
|
||||||
return { showToast }
|
|
||||||
}
|
|
||||||
39
apps/builder/src/hooks/useToast.tsx
Normal file
39
apps/builder/src/hooks/useToast.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Toast, ToastProps } from '@/components/Toast'
|
||||||
|
import { useToast as useChakraToast } from '@chakra-ui/react'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
export const useToast = () => {
|
||||||
|
const toast = useChakraToast()
|
||||||
|
|
||||||
|
const showToast = useCallback(
|
||||||
|
({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status = 'error',
|
||||||
|
icon,
|
||||||
|
details,
|
||||||
|
primaryButton,
|
||||||
|
secondaryButton,
|
||||||
|
}: Omit<ToastProps, 'onClose'>) => {
|
||||||
|
toast({
|
||||||
|
position: 'top-right',
|
||||||
|
duration: details ? null : undefined,
|
||||||
|
render: ({ onClose }) => (
|
||||||
|
<Toast
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
status={status}
|
||||||
|
icon={icon}
|
||||||
|
details={details}
|
||||||
|
onClose={onClose}
|
||||||
|
primaryButton={primaryButton}
|
||||||
|
secondaryButton={secondaryButton}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[toast]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { showToast }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
|||||||
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
|
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import {
|
import {
|
||||||
|
ChatReply,
|
||||||
SessionState,
|
SessionState,
|
||||||
Variable,
|
Variable,
|
||||||
VariableWithUnknowValue,
|
VariableWithUnknowValue,
|
||||||
@@ -19,7 +20,6 @@ import { updateVariables } from '@/features/variables/updateVariables'
|
|||||||
import { parseVariables } from '@/features/variables/parseVariables'
|
import { parseVariables } from '@/features/variables/parseVariables'
|
||||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||||
import { parseVariableNumber } from '@/features/variables/parseVariableNumber'
|
import { parseVariableNumber } from '@/features/variables/parseVariableNumber'
|
||||||
import { HTTPError } from 'got'
|
|
||||||
|
|
||||||
export const createChatCompletionOpenAI = async (
|
export const createChatCompletionOpenAI = async (
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
@@ -29,9 +29,15 @@ export const createChatCompletionOpenAI = async (
|
|||||||
}: { outgoingEdgeId?: string; options: ChatCompletionOpenAIOptions }
|
}: { outgoingEdgeId?: string; options: ChatCompletionOpenAIOptions }
|
||||||
): Promise<ExecuteIntegrationResponse> => {
|
): Promise<ExecuteIntegrationResponse> => {
|
||||||
let newSessionState = state
|
let newSessionState = state
|
||||||
|
const noCredentialsError = {
|
||||||
|
status: 'error',
|
||||||
|
description: 'Make sure to select an OpenAI account',
|
||||||
|
}
|
||||||
if (!options.credentialsId) {
|
if (!options.credentialsId) {
|
||||||
console.error('OpenAI block has no credentials')
|
return {
|
||||||
return { outgoingEdgeId }
|
outgoingEdgeId,
|
||||||
|
logs: [noCredentialsError],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const credentials = await prisma.credentials.findUnique({
|
const credentials = await prisma.credentials.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -40,7 +46,7 @@ export const createChatCompletionOpenAI = async (
|
|||||||
})
|
})
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
console.error('Could not find credentials in database')
|
console.error('Could not find credentials in database')
|
||||||
return { outgoingEdgeId }
|
return { outgoingEdgeId, logs: [noCredentialsError] }
|
||||||
}
|
}
|
||||||
const { apiKey } = decrypt(
|
const { apiKey } = decrypt(
|
||||||
credentials.data,
|
credentials.data,
|
||||||
@@ -107,21 +113,24 @@ export const createChatCompletionOpenAI = async (
|
|||||||
newSessionState,
|
newSessionState,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const log = {
|
const log: NonNullable<ChatReply['logs']>[number] = {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
description: 'OpenAI block returned error',
|
description: 'OpenAI block returned error',
|
||||||
details: '',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof HTTPError) {
|
if (err && typeof err === 'object') {
|
||||||
console.error(err.response.body)
|
if ('response' in err) {
|
||||||
log.details = JSON.stringify(err.response.body, null, 2).substring(
|
const { status, data } = err.response as {
|
||||||
0,
|
status: string
|
||||||
1000
|
data: string
|
||||||
)
|
}
|
||||||
} else {
|
log.details = {
|
||||||
console.error(err)
|
status,
|
||||||
log.details = JSON.stringify(err, null, 2).substring(0, 1000)
|
data,
|
||||||
|
}
|
||||||
|
} else if ('message' in err) {
|
||||||
|
log.details = err.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.result &&
|
state.result &&
|
||||||
|
|||||||
Reference in New Issue
Block a user