2
0

Better error toast when previewing bot

Closes #475
This commit is contained in:
Baptiste Arnaud
2023-04-27 11:21:32 +02:00
parent 3c6a666d9b
commit d448e64dc9
7 changed files with 235 additions and 40 deletions

View 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 />
}
}

View File

@ -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>
)

View File

@ -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',
},

View File

@ -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)
})
}

View File

@ -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 }
}

View 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 }
}

View File

@ -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 &&