2
0

🚸 Display error toast when script or set vari…

This commit is contained in:
Baptiste Arnaud
2024-06-11 18:18:05 +02:00
parent d80b3eaddb
commit 233ff91a57
15 changed files with 151 additions and 50 deletions

View File

@@ -14,6 +14,7 @@ type Props<T extends string> = {
value?: T value?: T
defaultValue?: T defaultValue?: T
direction?: 'row' | 'column' direction?: 'row' | 'column'
size?: 'md' | 'sm'
onSelect: (newValue: T) => void onSelect: (newValue: T) => void
} }
export const RadioButtons = <T extends string>({ export const RadioButtons = <T extends string>({
@@ -21,6 +22,7 @@ export const RadioButtons = <T extends string>({
value, value,
defaultValue, defaultValue,
direction = 'row', direction = 'row',
size = 'md',
onSelect, onSelect,
}: Props<T>) => { }: Props<T>) => {
const { getRootProps, getRadioProps } = useRadioGroup({ const { getRootProps, getRadioProps } = useRadioGroup({
@@ -36,7 +38,7 @@ export const RadioButtons = <T extends string>({
{options.map((item) => { {options.map((item) => {
const radio = getRadioProps({ value: parseValue(item) }) const radio = getRadioProps({ value: parseValue(item) })
return ( return (
<RadioCard key={parseValue(item)} {...radio}> <RadioCard key={parseValue(item)} {...radio} size={size}>
{parseLabel(item)} {parseLabel(item)}
</RadioCard> </RadioCard>
) )
@@ -45,7 +47,9 @@ export const RadioButtons = <T extends string>({
) )
} }
export const RadioCard = (props: UseRadioProps & { children: ReactNode }) => { export const RadioCard = (
props: UseRadioProps & { children: ReactNode; size?: 'md' | 'sm' }
) => {
const { getInputProps, getCheckboxProps } = useRadio(props) const { getInputProps, getCheckboxProps } = useRadio(props)
const input = getInputProps() const input = getInputProps()
@@ -68,10 +72,11 @@ export const RadioCard = (props: UseRadioProps & { children: ReactNode }) => {
_active={{ _active={{
bgColor: useColorModeValue('gray.200', 'gray.600'), bgColor: useColorModeValue('gray.200', 'gray.600'),
}} }}
px={5} px={props.size === 'sm' ? 3 : 5}
py={2} py={props.size === 'sm' ? 1 : 2}
transition="background-color 150ms, color 150ms, border 150ms" transition="background-color 150ms, color 150ms, border 150ms"
justifyContent="center" justifyContent="center"
fontSize={props.size === 'sm' ? 'sm' : undefined}
> >
{props.children} {props.children}
</Flex> </Flex>

View File

@@ -15,6 +15,7 @@ import { TextInput, Textarea } from '@/components/inputs'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { isInputBlock } from '@typebot.io/schemas/helpers' import { isInputBlock } from '@typebot.io/schemas/helpers'
import { RadioButtons } from '@/components/inputs/RadioButtons'
type Props = { type Props = {
options: SetVariableBlock['options'] options: SetVariableBlock['options']
@@ -172,6 +173,14 @@ const SetVariableValue = ({
}) })
} }
const updateIsCode = (radio: 'Text' | 'Code') => {
if (options?.type && options.type !== 'Custom') return
onOptionsChange({
...options,
isCode: radio === 'Code',
})
}
switch (options?.type) { switch (options?.type) {
case 'Custom': case 'Custom':
case undefined: case undefined:
@@ -186,11 +195,31 @@ const SetVariableValue = ({
} }
onCheckChange={updateClientExecution} onCheckChange={updateClientExecution}
/> />
<CodeEditor <Stack>
defaultValue={options?.expressionToEvaluate ?? ''} <RadioButtons
onChange={updateExpression} size="sm"
lang="javascript" options={['Text', 'Code']}
/> defaultValue={
options?.isCode ?? defaultSetVariableOptions.isCode
? 'Code'
: 'Text'
}
onSelect={updateIsCode}
/>
{options?.isCode === undefined || options.isCode ? (
<CodeEditor
defaultValue={options?.expressionToEvaluate ?? ''}
onChange={updateExpression}
lang="javascript"
/>
) : (
<Textarea
defaultValue={options?.expressionToEvaluate ?? ''}
onChange={updateExpression}
width="full"
/>
)}
</Stack>
</> </>
) )
case 'Map item with same index': { case 'Map item with same index': {

View File

@@ -12396,6 +12396,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"isCode": {
"type": "boolean"
},
"args": { "args": {
"type": "array", "type": "array",
"items": { "items": {
@@ -12509,6 +12512,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"isCode": {
"type": "boolean"
},
"args": { "args": {
"type": "array", "type": "array",
"items": { "items": {
@@ -12677,6 +12683,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"isCode": {
"type": "boolean"
},
"args": { "args": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -17,8 +17,12 @@ import {
parseTranscriptMessageText, parseTranscriptMessageText,
} from '@typebot.io/logic/computeResultTranscript' } from '@typebot.io/logic/computeResultTranscript'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants' import {
defaultSetVariableOptions,
sessionOnlySetVariableOptions,
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import { createCodeRunner } from '@typebot.io/variables/codeRunners' import { createCodeRunner } from '@typebot.io/variables/codeRunners'
import { stringifyError } from '@typebot.io/lib/stringifyError'
export const executeSetVariable = async ( export const executeSetVariable = async (
state: SessionState, state: SessionState,
@@ -34,6 +38,9 @@ export const executeSetVariable = async (
block.id block.id
) )
const isCustomValue = !block.options.type || block.options.type === 'Custom' const isCustomValue = !block.options.type || block.options.type === 'Custom'
const isCode =
(!block.options.type || block.options.type === 'Custom') &&
(block.options.isCode ?? defaultSetVariableOptions.isCode)
if ( if (
expressionToEvaluate && expressionToEvaluate &&
!state.whatsApp && !state.whatsApp &&
@@ -50,21 +57,25 @@ export const executeSetVariable = async (
{ {
type: 'setVariable', type: 'setVariable',
setVariable: { setVariable: {
scriptToExecute, scriptToExecute: {
...scriptToExecute,
isCode,
},
}, },
expectsDedicatedReply: true, expectsDedicatedReply: true,
}, },
], ],
} }
} }
const evaluatedExpression = expressionToEvaluate const { value, error } =
? evaluateSetVariableExpression(variables)(expressionToEvaluate) (expressionToEvaluate
: undefined ? evaluateSetVariableExpression(variables)(expressionToEvaluate)
: undefined) ?? {}
const existingVariable = variables.find(byId(block.options.variableId)) const existingVariable = variables.find(byId(block.options.variableId))
if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId } if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId }
const newVariable = { const newVariable = {
...existingVariable, ...existingVariable,
value: evaluatedExpression, value,
} }
const { newSetVariableHistory, updatedState } = updateVariablesInSession({ const { newSetVariableHistory, updatedState } = updateVariablesInSession({
state, state,
@@ -85,24 +96,40 @@ export const executeSetVariable = async (
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
newSessionState: updatedState, newSessionState: updatedState,
newSetVariableHistory, newSetVariableHistory,
logs:
error && isCode
? [
{
status: 'error',
description: 'Error evaluating Set variable code',
details: error,
},
]
: undefined,
} }
} }
const evaluateSetVariableExpression = const evaluateSetVariableExpression =
(variables: Variable[]) => (variables: Variable[]) =>
(str: string): unknown => { (str: string): { value: unknown; error?: string } => {
const isSingleVariable = const isSingleVariable =
str.startsWith('{{') && str.endsWith('}}') && str.split('{{').length === 2 str.startsWith('{{') && str.endsWith('}}') && str.split('{{').length === 2
if (isSingleVariable) return parseVariables(variables)(str) if (isSingleVariable) return { value: parseVariables(variables)(str) }
// To avoid octal number evaluation // To avoid octal number evaluation
if (!isNaN(str as unknown as number) && /0[^.].+/.test(str)) return str if (!isNaN(str as unknown as number) && /0[^.].+/.test(str))
return { value: str }
try { try {
const body = parseVariables(variables, { fieldToParse: 'id' })(str) const body = parseVariables(variables, { fieldToParse: 'id' })(str)
return createCodeRunner({ variables })( return {
body.includes('return ') ? body : `return ${body}` value: createCodeRunner({ variables })(
) body.includes('return ') ? body : `return ${body}`
),
}
} catch (err) { } catch (err) {
return parseVariables(variables)(str) return {
value: parseVariables(variables)(str),
error: stringifyError(err),
}
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.2.86", "version": "0.2.87",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -131,18 +131,18 @@ export const ConversationContainer = (props: Props) => {
) )
}) })
const sendMessage = async ( const saveLogs = async (clientLogs?: ChatLog[]) => {
message: string | undefined, if (!clientLogs) return
clientLogs?: ChatLog[] props.onNewLogs?.(clientLogs)
) => { if (props.context.isPreview) return
if (clientLogs) { await saveClientLogsQuery({
props.onNewLogs?.(clientLogs) apiHost: props.context.apiHost,
await saveClientLogsQuery({ sessionId: props.initialChatReply.sessionId,
apiHost: props.context.apiHost, clientLogs,
sessionId: props.initialChatReply.sessionId, })
clientLogs, }
})
} const sendMessage = async (message: string | undefined) => {
setHasError(false) setHasError(false)
const currentInputBlock = [...chatChunks()].pop()?.input const currentInputBlock = [...chatChunks()].pop()?.input
if (currentInputBlock?.id && props.onAnswer && message) if (currentInputBlock?.id && props.onAnswer && message)
@@ -288,9 +288,10 @@ export const ConversationContainer = (props: Props) => {
}, },
onMessageStream: streamMessage, onMessageStream: streamMessage,
}) })
if (response && 'logs' in response) saveLogs(response.logs)
if (response && 'replyToSend' in response) { if (response && 'replyToSend' in response) {
setIsSending(false) setIsSending(false)
sendMessage(response.replyToSend, response.logs) sendMessage(response.replyToSend)
return return
} }
if (response && 'blockedPopupUrl' in response) if (response && 'blockedPopupUrl' in response)

View File

@@ -1,9 +1,13 @@
import type { ScriptToExecute } from '@typebot.io/schemas' import { stringifyError } from '@typebot.io/lib/stringifyError'
import type { ChatLog, ScriptToExecute } from '@typebot.io/schemas'
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
export const executeScript = async ({ content, args }: ScriptToExecute) => { export const executeScript = async ({
content,
args,
}: ScriptToExecute): Promise<void | { logs: ChatLog[] }> => {
try { try {
const func = AsyncFunction( const func = AsyncFunction(
...args.map((arg) => arg.id), ...args.map((arg) => arg.id),
@@ -11,7 +15,15 @@ export const executeScript = async ({ content, args }: ScriptToExecute) => {
) )
await func(...args.map((arg) => arg.value)) await func(...args.map((arg) => arg.value))
} catch (err) { } catch (err) {
console.warn('Script threw an error:', err) return {
logs: [
{
status: 'error',
description: 'Script block failed to execute',
details: stringifyError(err),
},
],
}
} }
} }

View File

@@ -1,5 +1,6 @@
import type { ScriptToExecute } from '@typebot.io/schemas' import type { ChatLog, ScriptToExecute } from '@typebot.io/schemas'
import { safeStringify } from '@typebot.io/lib/safeStringify' import { safeStringify } from '@typebot.io/lib/safeStringify'
import { stringifyError } from '@typebot.io/lib/stringifyError'
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
@@ -7,7 +8,11 @@ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
export const executeSetVariable = async ({ export const executeSetVariable = async ({
content, content,
args, args,
}: ScriptToExecute): Promise<{ replyToSend: string | undefined }> => { isCode,
}: ScriptToExecute): Promise<{
replyToSend: string | undefined
logs?: ChatLog[]
}> => {
try { try {
// To avoid octal number evaluation // To avoid octal number evaluation
if (!isNaN(content as unknown as number) && /0[^.].+/.test(content)) if (!isNaN(content as unknown as number) && /0[^.].+/.test(content))
@@ -26,6 +31,15 @@ export const executeSetVariable = async ({
console.error(err) console.error(err)
return { return {
replyToSend: safeStringify(content) ?? undefined, replyToSend: safeStringify(content) ?? undefined,
logs: isCode
? [
{
status: 'error',
description: 'Failed to execute Set Variable code',
details: stringifyError(err),
},
]
: undefined,
} }
} }
} }

View File

@@ -27,6 +27,7 @@ export const executeClientSideAction = async ({
}: Props): Promise< }: Props): Promise<
| { blockedPopupUrl: string } | { blockedPopupUrl: string }
| { replyToSend: string | undefined; logs?: ChatLog[] } | { replyToSend: string | undefined; logs?: ChatLog[] }
| { logs: ChatLog[] }
| void | void
> => { > => {
if ('chatwoot' in clientSideAction) { if ('chatwoot' in clientSideAction) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.2.86", "version": "0.2.87",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.2.86", "version": "0.2.87",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -0,0 +1,6 @@
export const stringifyError = (err: unknown): string =>
typeof err === 'string'
? err
: err instanceof Error
? err.name + ': ' + err.message
: JSON.stringify(err)

View File

@@ -26,4 +26,5 @@ export const sessionOnlySetVariableOptions = ['Transcript'] as const
export const defaultSetVariableOptions = { export const defaultSetVariableOptions = {
type: 'Custom', type: 'Custom',
isExecutedOnClient: false, isExecutedOnClient: false,
isCode: false,
} as const satisfies SetVariableBlock['options'] } as const satisfies SetVariableBlock['options']

View File

@@ -21,6 +21,7 @@ export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>
const scriptToExecuteSchema = z.object({ const scriptToExecuteSchema = z.object({
content: z.string(), content: z.string(),
isCode: z.boolean().optional(),
args: z.array( args: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),

View File

@@ -6,6 +6,7 @@ import { safeStringify } from '@typebot.io/lib/safeStringify'
import { Variable } from './types' import { Variable } from './types'
import ivm from 'isolated-vm' import ivm from 'isolated-vm'
import { parseTransferrableValue } from './codeRunners' import { parseTransferrableValue } from './codeRunners'
import { stringifyError } from '@typebot.io/lib/stringifyError'
const defaultTimeout = 10 * 1000 const defaultTimeout = 10 * 1000
@@ -74,7 +75,6 @@ export const executeFunction = async ({
try { try {
const output = await run(parsedBody) const output = await run(parsedBody)
console.log('Output', output)
return { return {
output: safeStringify(output) ?? '', output: safeStringify(output) ?? '',
newVariables: Object.entries(updatedVariables) newVariables: Object.entries(updatedVariables)
@@ -93,12 +93,7 @@ export const executeFunction = async ({
console.log('Error while executing script') console.log('Error while executing script')
console.error(e) console.error(e)
const error = const error = stringifyError(e)
typeof e === 'string'
? e
: e instanceof Error
? e.message
: JSON.stringify(e)
return { return {
error, error,