diff --git a/apps/builder/src/components/Toast.tsx b/apps/builder/src/components/Toast.tsx new file mode 100644 index 0000000000..5a47cd431b --- /dev/null +++ b/apps/builder/src/components/Toast.tsx @@ -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 ( + + + {' '} + + + {title && {title}} + {description && {description}} + + + {details && ( + + )} + {(secondaryButton || primaryButton) && ( + + {secondaryButton} + {primaryButton} + + )} + + + + } + size="sm" + onClick={onClose} + variant="ghost" + pos="absolute" + top={1} + right={1} + /> + + ) +} + +const Icon = ({ + customIcon, + status, +}: { + customIcon?: React.ReactNode + status: ToastProps['status'] +}) => { + const color = parseColor(status) + const icon = parseIcon(status, customIcon) + return ( + + + {icon} + + + ) +} + +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 + case 'success': + return + case 'info': + return + } +} diff --git a/apps/builder/src/components/icons.tsx b/apps/builder/src/components/icons.tsx index 4422fc2be4..9abc6ac5db 100644 --- a/apps/builder/src/components/icons.tsx +++ b/apps/builder/src/components/icons.tsx @@ -604,3 +604,20 @@ export const ShuffleIcon = (props: IconProps) => ( ) + +export const InfoIcon = (props: IconProps) => ( + + + + + +) + +export const SmileIcon = (props: IconProps) => ( + + + + + + +) diff --git a/apps/builder/src/components/inputs/CodeEditor.tsx b/apps/builder/src/components/inputs/CodeEditor.tsx index 43808fa38a..d0cdb53271 100644 --- a/apps/builder/src/components/inputs/CodeEditor.tsx +++ b/apps/builder/src/components/inputs/CodeEditor.tsx @@ -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', }, diff --git a/apps/builder/src/features/preview/components/WebPreview.tsx b/apps/builder/src/features/preview/components/WebPreview.tsx index dcce3af7c6..0d19ff1452 100644 --- a/apps/builder/src/features/preview/components/WebPreview.tsx +++ b/apps/builder/src/features/preview/components/WebPreview.tsx @@ -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: , + title: 'An error occured', + description: log.description, + details: log.details + ? { + lang: 'json', + content: JSON.stringify(log.details, null, 2), + } + : undefined, + }) console.error(log) }) } diff --git a/apps/builder/src/hooks/useToast.ts b/apps/builder/src/hooks/useToast.ts deleted file mode 100644 index 2e65e7d531..0000000000 --- a/apps/builder/src/hooks/useToast.ts +++ /dev/null @@ -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 } -} diff --git a/apps/builder/src/hooks/useToast.tsx b/apps/builder/src/hooks/useToast.tsx new file mode 100644 index 0000000000..7fd205fb5d --- /dev/null +++ b/apps/builder/src/hooks/useToast.tsx @@ -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) => { + toast({ + position: 'top-right', + duration: details ? null : undefined, + render: ({ onClose }) => ( + + ), + }) + }, + [toast] + ) + + return { showToast } +} diff --git a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts index cd2a37cdba..b056b82c61 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts @@ -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 => { 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[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 &&