Skip to content

Commit

Permalink
⚡ (setVariable) Add client-side set variable execution
Browse files Browse the repository at this point in the history
Closes #461
  • Loading branch information
baptisteArno committed Apr 14, 2023
1 parent 397a33a commit 03cc067
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
const [nextPage, setNextPage] = useState(0)

const fetchNewImages = useCallback(async (query: string, page: number) => {
console.log('Fetch images', query, page)
if (query === '') return searchRandomImages()
if (query.length <= 2) return
setError(null)
Expand Down Expand Up @@ -76,7 +75,6 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
if (!bottomAnchor.current) return
const observer = new IntersectionObserver(
(entities: IntersectionObserverEntry[]) => {
console.log('Intersection observer', entities)
const target = entities[0]
if (target.isIntersecting) fetchNewImages(searchQuery, nextPage + 1)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,40 @@ import { SetVariableOptions, Variable } from '@typebot.io/schemas'
import React from 'react'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Textarea } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'

type Props = {
options: SetVariableOptions
onOptionsChange: (options: SetVariableOptions) => void
}

export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
const handleVariableChange = (variable?: Variable) =>
const updateVariableId = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const handleExpressionChange = (expressionToEvaluate: string) =>

const updateExpression = (expressionToEvaluate: string) =>
onOptionsChange({ ...options, expressionToEvaluate })
const handleValueTypeChange = () =>

const updateExpressionType = () =>
onOptionsChange({
...options,
isCode: options.isCode ? !options.isCode : true,
})

const updateClientExecution = (isExecutedOnClient: boolean) =>
onOptionsChange({
...options,
isExecutedOnClient,
})

return (
<Stack spacing={4}>
<Stack>
<FormLabel mb="0" htmlFor="variable-search">
Search or create variable:
</FormLabel>
<VariableSearchInput
onSelectVariable={handleVariableChange}
onSelectVariable={updateVariableId}
initialVariableId={options.variableId}
id="variable-search"
/>
Expand All @@ -43,7 +52,7 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
<Switch
size="sm"
isChecked={options.isCode ?? false}
onChange={handleValueTypeChange}
onChange={updateExpressionType}
/>
<Text fontSize="sm">Code</Text>
</HStack>
Expand All @@ -52,17 +61,23 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
{options.isCode ?? false ? (
<CodeEditor
defaultValue={options.expressionToEvaluate ?? ''}
onChange={handleExpressionChange}
onChange={updateExpression}
lang="javascript"
/>
) : (
<Textarea
id="expression"
defaultValue={options.expressionToEvaluate ?? ''}
onChange={handleExpressionChange}
onChange={updateExpression}
/>
)}
</Stack>
<SwitchWithLabel
label="Execute on client?"
moreInfoContent="Check this if you need access to client-only variables like `window` or `document`."
initialValue={options.isExecutedOnClient ?? false}
onCheckChange={updateClientExecution}
/>
</Stack>
)
}
35 changes: 23 additions & 12 deletions apps/viewer/src/features/blocks/logic/script/executeScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
import { extractVariablesFromText } from '@/features/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
import { parseVariables } from '@/features/variables/parseVariables'
import { ScriptBlock, SessionState } from '@typebot.io/schemas'
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'

export const executeScript = (
{ typebot: { variables } }: SessionState,
Expand All @@ -11,26 +11,37 @@ export const executeScript = (
): ExecuteLogicResponse => {
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }

const content = parseVariables(variables, { fieldToParse: 'id' })(
const scriptToExecute = parseScriptToExecuteClientSideAction(
variables,
block.options.content
)
const args = extractVariablesFromText(variables)(block.options.content).map(
(variable) => ({
id: variable.id,
value: parseGuessedValueType(variable.value),
})
)

return {
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
scriptToExecute: {
content,
args,
},
scriptToExecute: scriptToExecute,
lastBubbleBlockId,
},
],
}
}

export const parseScriptToExecuteClientSideAction = (
variables: Variable[],
contentToEvaluate: string
) => {
const content = parseVariables(variables, { fieldToParse: 'id' })(
contentToEvaluate
)
const args = extractVariablesFromText(variables)(contentToEvaluate).map(
(variable) => ({
id: variable.id,
value: parseGuessedValueType(variable.value),
})
)
return {
content,
args,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,35 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
import { updateVariables } from '@/features/variables/updateVariables'
import { parseVariables } from '@/features/variables/parseVariables'
import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'

export const executeSetVariable = async (
state: SessionState,
block: SetVariableBlock
block: SetVariableBlock,
lastBubbleBlockId?: string
): Promise<ExecuteLogicResponse> => {
const { variables } = state.typebot
if (!block.options?.variableId)
return {
outgoingEdgeId: block.outgoingEdgeId,
}
if (block.options.isExecutedOnClient && block.options.expressionToEvaluate) {
const scriptToExecute = parseScriptToExecuteClientSideAction(
state.typebot.variables,
block.options.expressionToEvaluate
)
return {
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
setVariable: {
scriptToExecute,
},
lastBubbleBlockId,
},
],
}
}
const evaluatedExpression = block.options.expressionToEvaluate
? evaluateSetVariableExpression(variables)(
block.options.expressionToEvaluate
Expand Down
42 changes: 19 additions & 23 deletions apps/viewer/src/features/chat/api/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@ export const sendMessage = publicProcedure
},
})

const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => 'setVariable' in action
)

if (
!input &&
!containsSetVariableClientSideAction &&
session.state.result.answers.length > 0 &&
session.state.result.id
)
Expand Down Expand Up @@ -149,42 +154,33 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
dynamicTheme: parseDynamicThemeInState(typebot.theme),
}

const {
messages,
input,
clientSideActions,
newSessionState: newInitialState,
logs,
} = await startBotFlow(initialState, startParams.startGroupId)
const { messages, input, clientSideActions, newSessionState, logs } =
await startBotFlow(initialState, startParams.startGroupId)

if (!input)
const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => 'setVariable' in action
)

if (!input && !containsSetVariableClientSideAction)
return {
messages,
clientSideActions,
typebot: {
id: typebot.id,
settings: deepParseVariables(newInitialState.typebot.variables)(
settings: deepParseVariables(newSessionState.typebot.variables)(
typebot.settings
),
theme: deepParseVariables(newInitialState.typebot.variables)(
theme: deepParseVariables(newSessionState.typebot.variables)(
typebot.theme
),
},
dynamicTheme: parseDynamicThemeReply(newInitialState),
dynamicTheme: parseDynamicThemeReply(newSessionState),
logs,
}

const sessionState: ChatSession['state'] = {
...newInitialState,
currentBlock: {
groupId: input.groupId,
blockId: input.id,
},
}

const session = (await prisma.chatSession.create({
data: {
state: sessionState,
state: newSessionState,
},
})) as ChatSession

Expand All @@ -193,17 +189,17 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
sessionId: session.id,
typebot: {
id: typebot.id,
settings: deepParseVariables(newInitialState.typebot.variables)(
settings: deepParseVariables(newSessionState.typebot.variables)(
typebot.settings
),
theme: deepParseVariables(newInitialState.typebot.variables)(
theme: deepParseVariables(newSessionState.typebot.variables)(
typebot.theme
),
},
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicThemeReply(newInitialState),
dynamicTheme: parseDynamicThemeReply(newSessionState),
logs,
} satisfies ChatReply
}
Expand Down
39 changes: 27 additions & 12 deletions apps/viewer/src/features/chat/helpers/continueBotFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
ChatReply,
InputBlock,
InputBlockType,
LogicBlockType,
ResultInSession,
SessionState,
SetVariableBlock,
} from '@typebot.io/schemas'
import { isInputBlock, isNotDefined } from '@typebot.io/lib'
import { isInputBlock, isNotDefined, byId, isDefined } from '@typebot.io/lib'
import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup'
import { validateEmail } from '@/features/blocks/inputs/email/validateEmail'
Expand All @@ -27,6 +29,7 @@ export const continueBotFlow =
async (
reply?: string
): Promise<ChatReply & { newSessionState?: SessionState }> => {
let newSessionState = { ...state }
const group = state.typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
)
Expand All @@ -43,24 +46,36 @@ export const continueBotFlow =
message: 'Current block not found',
})

if (!isInputBlock(block))
if (block.type === LogicBlockType.SET_VARIABLE && isDefined(reply)) {
const existingVariable = state.typebot.variables.find(
byId(block.options.variableId)
)
if (existingVariable) {
const newVariable = {
...existingVariable,
value: reply,
}
newSessionState = await updateVariables(state)([newVariable])
}
} else if (!isInputBlock(block))
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Current block is not an input block',
})

if (reply && !isReplyValid(reply, block)) return parseRetryMessage(block)
let formattedReply = null

const formattedReply = formatReply(reply, block.type)
if (isInputBlock(block)) {
if (reply && !isReplyValid(reply, block)) return parseRetryMessage(block)

if (!formattedReply && !canSkip(block.type)) {
return parseRetryMessage(block)
}
formattedReply = formatReply(reply, block.type)

const newSessionState = await processAndSaveAnswer(
state,
block
)(formattedReply)
if (!formattedReply && !canSkip(block.type)) {
return parseRetryMessage(block)
}

newSessionState = await processAndSaveAnswer(state, block)(formattedReply)
}

const groupHasMoreBlocks = blockIndex < group.blocks.length - 1

Expand Down Expand Up @@ -221,7 +236,7 @@ const computeStorageUsed = async (reply: string) => {

const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
(block: InputBlock, reply: string | null) => {
(block: InputBlock | SetVariableBlock, reply: string | null) => {
if (
block.type === InputBlockType.CHOICE &&
!block.options.isMultipleChoice &&
Expand Down
22 changes: 21 additions & 1 deletion apps/viewer/src/features/chat/helpers/executeGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,31 @@ export const executeGroup =
if (
'clientSideActions' in executionResponse &&
executionResponse.clientSideActions
)
) {
clientSideActions = [
...(clientSideActions ?? []),
...executionResponse.clientSideActions,
]
if (
executionResponse.clientSideActions?.find(
(action) => 'setVariable' in action
)
) {
return {
messages,
newSessionState: {
...newSessionState,
currentBlock: {
groupId: group.id,
blockId: block.id,
},
},
clientSideActions,
logs,
}
}
}

if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
Expand Down
2 changes: 1 addition & 1 deletion apps/viewer/src/features/chat/helpers/executeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const executeLogic =
async (block: LogicBlock): Promise<ExecuteLogicResponse> => {
switch (block.type) {
case LogicBlockType.SET_VARIABLE:
return executeSetVariable(state, block)
return executeSetVariable(state, block, lastBubbleBlockId)
case LogicBlockType.CONDITION:
return executeCondition(state, block)
case LogicBlockType.REDIRECT:
Expand Down
Loading

4 comments on commit 03cc067

@vercel
Copy link

@vercel vercel bot commented on 03cc067 Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

filmylogy.com
goldorayo.com
rabbit.cr8.ai
signup.cr8.ai
start.taxt.co
turkey.cr8.ai
vhpage.cr8.ai
vitamyway.com
am.nigerias.io
an.nigerias.io
app.yvon.earth
ar.nigerias.io
bot.enreso.org
bot.rslabs.pro
bots.bridge.ai
chat.hayuri.id
chatgpt.lam.ee
chicken.cr8.ai
gollum.riku.ai
gsbulletin.com
journey.cr8.ai
panther.cr7.ai
panther.cr8.ai
pay.sifuim.com
penguin.cr8.ai
talk.gocare.io
test.bot.gives
ticketfute.com
unicorn.cr8.ai
apo.nigerias.io
apr.nigerias.io
aso.nigerias.io
blackcan.cr8.ai
bot.4display.nl
bot.ageenda.com
bot.artiweb.app
bot.devitus.com
bot.jesopizz.it
bot.reeplai.com
bot.scayver.com
bot.tc-mail.com
chat.lalmon.com
chat.sureb4.com
eventhub.com.au
fitness.riku.ai
games.klujo.com
sakuranembro.it
typebot.aloe.do
aplicacao.bmind.com.br
apply.ansuraniphone.my
bbutton.wpwwakanda.com
bolsamaisbrasil.app.br
bot.ilmuseoaiborghi.it
bot.louismarcondes.com
bot.pratikmandalia.com
bot.t20worldcup.com.au
bot2.mycompany.reviews
type.dericsoncalari.com.br
bot.pinpointinteractive.com
bot.polychromes-project.com
bot.seidinembroseanchetu.it
chat.semanalimpanome.com.br
designguide.techyscouts.com
liveconvert2.kandalearn.com
presente.empresarias.com.mx
register.algorithmpress.com
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
download.venturemarketing.in
piazzatorre.barrettamario.it
type.cookieacademyonline.com
upload.atlasoutfittersk9.com
bot.brigadeirosemdrama.com.br
forms.escoladeautomacao.com.br
onboarding.libertydreamcare.ie
type.talitasouzamarques.com.br
agendamento.sergiolimajr.com.br
anamnese.clinicamegasjdr.com.br
bookings.littlepartymonkeys.com
bot.comercializadoraomicron.com
elevateyourmind.groovepages.com
viewer-v2-typebot-io.vercel.app
yourfeedback.comebackreward.com
bot.cabin-rentals-of-georgia.net
gerador.verificadordehospedes.com
personal-trainer.barrettamario.it
preagendamento.sergiolimajr.com.br
studiotecnicoimmobiliaremerelli.it
download.thailandmicespecialist.com
register.thailandmicespecialist.com
bot.studiotecnicoimmobiliaremerelli.it
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
chrome-os-inquiry-system.itschromeos.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com

@vercel
Copy link

@vercel vercel bot commented on 03cc067 Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 03cc067 Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-git-main-typebot-io.vercel.app
docs.typebot.io
docs-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 03cc067 Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

app.typebot.io
builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app

Please sign in to comment.