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>
|
||||
</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
|
||||
withVariableButton?: boolean
|
||||
height?: string
|
||||
maxHeight?: string
|
||||
onChange?: (value: string) => void
|
||||
}
|
||||
export const CodeEditor = ({
|
||||
@ -32,6 +33,7 @@ export const CodeEditor = ({
|
||||
lang,
|
||||
onChange,
|
||||
height = '250px',
|
||||
maxHeight = '70vh',
|
||||
withVariableButton = true,
|
||||
isReadOnly = false,
|
||||
debounceTimeout = 1000,
|
||||
@ -93,9 +95,10 @@ export const CodeEditor = ({
|
||||
pos="relative"
|
||||
onMouseEnter={onOpen}
|
||||
onMouseLeave={onClose}
|
||||
maxWidth={props.maxWidth}
|
||||
sx={{
|
||||
'& .cm-editor': {
|
||||
maxH: '70vh',
|
||||
maxH: maxHeight,
|
||||
outline: '0px solid transparent !important',
|
||||
rounded: 'md',
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { WebhookIcon } from '@/components/icons'
|
||||
import { useEditor } from '@/features/editor/providers/EditorProvider'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { UseToastOptions } from '@chakra-ui/react'
|
||||
import { Standard } from '@typebot.io/react'
|
||||
import { ChatReply } from '@typebot.io/schemas'
|
||||
|
||||
@ -15,7 +15,17 @@ export const WebPreview = () => {
|
||||
|
||||
const handleNewLogs = (logs: ChatReply['logs']) => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
@ -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 prisma from '@/lib/prisma'
|
||||
import {
|
||||
ChatReply,
|
||||
SessionState,
|
||||
Variable,
|
||||
VariableWithUnknowValue,
|
||||
@ -19,7 +20,6 @@ import { updateVariables } from '@/features/variables/updateVariables'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { parseVariableNumber } from '@/features/variables/parseVariableNumber'
|
||||
import { HTTPError } from 'got'
|
||||
|
||||
export const createChatCompletionOpenAI = async (
|
||||
state: SessionState,
|
||||
@ -29,9 +29,15 @@ export const createChatCompletionOpenAI = async (
|
||||
}: { outgoingEdgeId?: string; options: ChatCompletionOpenAIOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
let newSessionState = state
|
||||
const noCredentialsError = {
|
||||
status: 'error',
|
||||
description: 'Make sure to select an OpenAI account',
|
||||
}
|
||||
if (!options.credentialsId) {
|
||||
console.error('OpenAI block has no credentials')
|
||||
return { outgoingEdgeId }
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
logs: [noCredentialsError],
|
||||
}
|
||||
}
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: {
|
||||
@ -40,7 +46,7 @@ export const createChatCompletionOpenAI = async (
|
||||
})
|
||||
if (!credentials) {
|
||||
console.error('Could not find credentials in database')
|
||||
return { outgoingEdgeId }
|
||||
return { outgoingEdgeId, logs: [noCredentialsError] }
|
||||
}
|
||||
const { apiKey } = decrypt(
|
||||
credentials.data,
|
||||
@ -107,21 +113,24 @@ export const createChatCompletionOpenAI = async (
|
||||
newSessionState,
|
||||
}
|
||||
} catch (err) {
|
||||
const log = {
|
||||
const log: NonNullable<ChatReply['logs']>[number] = {
|
||||
status: 'error',
|
||||
description: 'OpenAI block returned error',
|
||||
details: '',
|
||||
}
|
||||
|
||||
if (err instanceof HTTPError) {
|
||||
console.error(err.response.body)
|
||||
log.details = JSON.stringify(err.response.body, null, 2).substring(
|
||||
0,
|
||||
1000
|
||||
)
|
||||
} else {
|
||||
console.error(err)
|
||||
log.details = JSON.stringify(err, null, 2).substring(0, 1000)
|
||||
if (err && typeof err === 'object') {
|
||||
if ('response' in err) {
|
||||
const { status, data } = err.response as {
|
||||
status: string
|
||||
data: string
|
||||
}
|
||||
log.details = {
|
||||
status,
|
||||
data,
|
||||
}
|
||||
} else if ('message' in err) {
|
||||
log.details = err.message
|
||||
}
|
||||
}
|
||||
|
||||
state.result &&
|
||||
|
Reference in New Issue
Block a user