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