2
0

🔥 Remove disable response saving option

Doesn't work properly when it comes to keep tracking storage usage
This commit is contained in:
Baptiste Arnaud
2023-03-07 14:41:57 +01:00
parent 0c19ea20f8
commit b77e2c8d2c
26 changed files with 194 additions and 182 deletions

View File

@ -54,7 +54,7 @@ export const Graph = ({
} = useGraph() } = useGraph()
const { updateGroupCoordinates } = useGroupsCoordinates() const { updateGroupCoordinates } = useGroupsCoordinates()
const [graphPosition, setGraphPosition] = useState( const [graphPosition, setGraphPosition] = useState(
graphPositionDefaultValue(typebot.groups[0].graphCoordinates) graphPositionDefaultValue(typebot.groups[0]?.graphCoordinates)
) )
const [debouncedGraphPosition] = useDebounce(graphPosition, 200) const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
const transform = useMemo( const transform = useMemo(

View File

@ -40,7 +40,7 @@ export const ItemNodesList = ({
const isLastBlock = const isLastBlock =
isDefined(typebot) && isDefined(typebot) &&
typebot.groups[groupIndex].blocks[blockIndex + 1] === undefined typebot.groups[groupIndex]?.blocks?.[blockIndex + 1] === undefined
const [position, setPosition] = useState({ const [position, setPosition] = useState({
x: 0, x: 0,

View File

@ -46,12 +46,6 @@ export const GeneralSettingsForm = ({
isHideQueryParamsEnabled, isHideQueryParamsEnabled,
}) })
const handleDisableResultsSavingChange = (isResultSavingEnabled: boolean) =>
onGeneralSettingsChange({
...generalSettings,
isResultSavingEnabled: !isResultSavingEnabled,
})
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
<ChangePlanModal <ChangePlanModal
@ -96,16 +90,6 @@ export const GeneralSettingsForm = ({
onCheckChange={handleHideQueryParamsChange} onCheckChange={handleHideQueryParamsChange}
moreInfoContent="If your URL contains query params, they will be automatically hidden when the bot starts." moreInfoContent="If your URL contains query params, they will be automatically hidden when the bot starts."
/> />
<SwitchWithLabel
label="Disable responses saving"
initialValue={
isDefined(generalSettings.isResultSavingEnabled)
? !generalSettings.isResultSavingEnabled
: false
}
onCheckChange={handleDisableResultsSavingChange}
moreInfoContent="Prevent responses from being saved on Typebot. Chats limit usage will still be tracked."
/>
</Stack> </Stack>
) )
} }

View File

@ -24,11 +24,6 @@ test.describe.parallel('Settings page', () => {
page.locator('input[type="checkbox"] >> nth=-3') page.locator('input[type="checkbox"] >> nth=-3')
).toHaveAttribute('checked', '') ).toHaveAttribute('checked', '')
await page.click('text="Disable responses saving"')
await expect(
page.locator('input[type="checkbox"] >> nth=-1')
).toHaveAttribute('checked', '')
await expect(page.getByPlaceholder('Type your answer...')).toHaveValue( await expect(page.getByPlaceholder('Type your answer...')).toHaveValue(
'Baptiste' 'Baptiste'
) )

View File

@ -1839,6 +1839,12 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": {
"type": "string"
},
"company": {
"type": "string"
},
"workspaceId": { "workspaceId": {
"type": "string" "type": "string"
}, },
@ -1867,9 +1873,27 @@
}, },
"additionalStorage": { "additionalStorage": {
"type": "number" "type": "number"
},
"vat": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"type",
"value"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
"email",
"company",
"workspaceId", "workspaceId",
"currency", "currency",
"plan", "plan",

View File

@ -3009,9 +3009,6 @@
}, },
"isNewResultOnRefreshEnabled": { "isNewResultOnRefreshEnabled": {
"type": "boolean" "type": "boolean"
},
"isResultSavingEnabled": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@ -4993,9 +4990,6 @@
}, },
"isNewResultOnRefreshEnabled": { "isNewResultOnRefreshEnabled": {
"type": "boolean" "type": "boolean"
},
"isResultSavingEnabled": {
"type": "boolean"
} }
}, },
"required": [ "required": [

View File

@ -107,8 +107,6 @@ export const TypebotPageV2 = ({
const sendNewVariables = const sendNewVariables =
(resultId: string) => async (variables: VariableWithValue[]) => { (resultId: string) => async (variables: VariableWithValue[]) => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
const { error } = await updateResultQuery(resultId, { variables }) const { error } = await updateResultQuery(resultId, { variables })
if (error) setError(error) if (error) setError(error)
} }
@ -117,10 +115,8 @@ export const TypebotPageV2 = ({
answer: AnswerInput & { uploadedFiles: boolean } answer: AnswerInput & { uploadedFiles: boolean }
) => { ) => {
if (!resultId) return setError(new Error('Error: result was not created')) if (!resultId) return setError(new Error('Error: result was not created'))
if (publishedTypebot.settings.general.isResultSavingEnabled !== false) { const { error } = await upsertAnswerQuery({ ...answer, resultId })
const { error } = await upsertAnswerQuery({ ...answer, resultId }) if (error) setError(error)
if (error) setError(error)
}
if (chatStarted) return if (chatStarted) return
updateResultQuery(resultId, { updateResultQuery(resultId, {
hasStarted: true, hasStarted: true,
@ -128,8 +124,6 @@ export const TypebotPageV2 = ({
} }
const handleCompleted = async () => { const handleCompleted = async () => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
if (!resultId) return setError(new Error('Error: result was not created')) if (!resultId) return setError(new Error('Error: result was not created'))
const { error } = await updateResultQuery(resultId, { isCompleted: true }) const { error } = await updateResultQuery(resultId, { isCompleted: true })
if (error) setError(error) if (error) setError(error)

View File

@ -11,17 +11,18 @@ import Stripe from 'stripe'
import { decrypt } from 'utils/api/encryption' import { decrypt } from 'utils/api/encryption'
export const computePaymentInputRuntimeOptions = export const computePaymentInputRuntimeOptions =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) => (state: Pick<SessionState, 'result' | 'typebot'>) =>
(options: PaymentInputOptions) => (options: PaymentInputOptions) =>
createStripePaymentIntent(state)(options) createStripePaymentIntent(state)(options)
const createStripePaymentIntent = const createStripePaymentIntent =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) => (state: Pick<SessionState, 'result' | 'typebot'>) =>
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => { async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
const { const {
isPreview, result,
typebot: { variables }, typebot: { variables },
} = state } = state
const isPreview = !result.id
if (!options.credentialsId) if (!options.credentialsId)
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',

View File

@ -50,10 +50,11 @@ if (window.$chatwoot) {
}` }`
export const executeChatwootBlock = ( export const executeChatwootBlock = (
{ typebot: { variables }, isPreview }: SessionState, { typebot: { variables }, result }: SessionState,
block: ChatwootBlock, block: ChatwootBlock,
lastBubbleBlockId?: string lastBubbleBlockId?: string
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
const isPreview = !result.id
const chatwootCode = parseChatwootOpenCode(block.options) const chatwootCode = parseChatwootOpenCode(block.options)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,

View File

@ -6,7 +6,7 @@ import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } from 'emails' import { DefaultBotNotificationEmail } from 'emails'
import { import {
PublicTypebot, PublicTypebot,
ResultValues, ResultInSession,
SendEmailBlock, SendEmailBlock,
SendEmailOptions, SendEmailOptions,
SessionState, SessionState,
@ -20,11 +20,12 @@ import { decrypt } from 'utils/api'
import { defaultFrom, defaultTransportOptions } from '../constants' import { defaultFrom, defaultTransportOptions } from '../constants'
export const executeSendEmailBlock = async ( export const executeSendEmailBlock = async (
{ result, typebot, isPreview }: SessionState, { result, typebot }: SessionState,
block: SendEmailBlock block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const { options } = block const { options } = block
const { variables } = typebot const { variables } = typebot
const isPreview = !result.id
if (isPreview) if (isPreview)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
@ -37,7 +38,7 @@ export const executeSendEmailBlock = async (
} }
await sendEmail({ await sendEmail({
typebotId: typebot.id, typebotId: typebot.id,
resultId: result?.id, result,
credentialsId: options.credentialsId, credentialsId: options.credentialsId,
recipients: options.recipients.map(parseVariables(variables)), recipients: options.recipients.map(parseVariables(variables)),
subject: parseVariables(variables)(options.subject ?? ''), subject: parseVariables(variables)(options.subject ?? ''),
@ -57,7 +58,7 @@ export const executeSendEmailBlock = async (
const sendEmail = async ({ const sendEmail = async ({
typebotId, typebotId,
resultId, result,
credentialsId, credentialsId,
recipients, recipients,
body, body,
@ -70,7 +71,7 @@ const sendEmail = async ({
fileUrls, fileUrls,
}: SendEmailOptions & { }: SendEmailOptions & {
typebotId: string typebotId: string
resultId?: string result: ResultInSession
fileUrls?: string | string[] fileUrls?: string | string[]
}) => { }) => {
const { name: replyToName } = parseEmailRecipient(replyTo) const { name: replyToName } = parseEmailRecipient(replyTo)
@ -94,12 +95,12 @@ const sendEmail = async ({
isCustomBody, isCustomBody,
isBodyCode, isBodyCode,
typebotId, typebotId,
resultId, result,
}) })
if (!emailBody) { if (!emailBody) {
await saveErrorLog({ await saveErrorLog({
resultId, resultId: result.id,
message: 'Email not sent', message: 'Email not sent',
details: { details: {
error: 'No email body found', error: 'No email body found',
@ -132,7 +133,7 @@ const sendEmail = async ({
try { try {
await transporter.sendMail(email) await transporter.sendMail(email)
await saveSuccessLog({ await saveSuccessLog({
resultId, resultId: result.id,
message: 'Email successfully sent', message: 'Email successfully sent',
details: { details: {
transportConfig: { transportConfig: {
@ -144,7 +145,7 @@ const sendEmail = async ({
}) })
} catch (err) { } catch (err) {
await saveErrorLog({ await saveErrorLog({
resultId, resultId: result.id,
message: 'Email not sent', message: 'Email not sent',
details: { details: {
error: err, error: err,
@ -182,10 +183,10 @@ const getEmailBody = async ({
isCustomBody, isCustomBody,
isBodyCode, isBodyCode,
typebotId, typebotId,
resultId, result,
}: { }: {
typebotId: string typebotId: string
resultId?: string result: ResultInSession
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise< } & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined { html?: string; text?: string } | undefined
> => { > => {
@ -198,12 +199,7 @@ const getEmailBody = async ({
where: { typebotId }, where: { typebotId },
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const resultValues = (await prisma.result.findUnique({ const answers = parseAnswers(typebot, [])(result)
where: { id: resultId },
include: { answers: true },
})) as ResultValues | null
if (!resultValues) return
const answers = parseAnswers(typebot, [])(resultValues)
return { return {
html: render( html: render(
<DefaultBotNotificationEmail <DefaultBotNotificationEmail

View File

@ -16,16 +16,15 @@ import {
WebhookOptions, WebhookOptions,
defaultWebhookAttributes, defaultWebhookAttributes,
HttpMethod, HttpMethod,
ResultValues,
PublicTypebot, PublicTypebot,
KeyValue, KeyValue,
ReplyLog, ReplyLog,
ResultInSession,
} from 'models' } from 'models'
import { stringify } from 'qs' import { stringify } from 'qs'
import { byId, omit } from 'utils' import { byId, omit } from 'utils'
import { parseAnswers } from 'utils/results' import { parseAnswers } from 'utils/results'
import got, { Method, Headers, HTTPError } from 'got' import got, { Method, Headers, HTTPError } from 'got'
import { getResultValues } from '@/features/results/api'
import { parseSampleResult } from './parseSampleResult' import { parseSampleResult } from './parseSampleResult'
export const executeWebhookBlock = async ( export const executeWebhookBlock = async (
@ -50,14 +49,11 @@ export const executeWebhookBlock = async (
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] } return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
} }
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const resultValues =
(result && (await getResultValues(result.id))) ?? undefined
const webhookResponse = await executeWebhook({ typebot })( const webhookResponse = await executeWebhook({ typebot })(
preparedWebhook, preparedWebhook,
typebot.variables, typebot.variables,
block.groupId, block.groupId,
resultValues, result
result?.id
) )
const status = webhookResponse.statusCode.toString() const status = webhookResponse.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5') const isError = status.startsWith('4') || status.startsWith('5')
@ -139,8 +135,7 @@ export const executeWebhook =
webhook: Webhook, webhook: Webhook,
variables: Variable[], variables: Variable[],
groupId: string, groupId: string,
resultValues?: ResultValues, result: ResultInSession
resultId?: string
): Promise<WebhookResponse> => { ): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method) if (!webhook.url || !webhook.method)
return { return {
@ -176,7 +171,7 @@ export const executeWebhook =
[] []
)({ )({
body: webhook.body, body: webhook.body,
resultValues, result,
groupId, groupId,
variables, variables,
}) })
@ -206,7 +201,7 @@ export const executeWebhook =
try { try {
const response = await got(request.url, omit(request, 'url')) const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog({ await saveSuccessLog({
resultId, resultId: result.id,
message: 'Webhook successfuly executed.', message: 'Webhook successfuly executed.',
details: { details: {
statusCode: response.statusCode, statusCode: response.statusCode,
@ -225,7 +220,7 @@ export const executeWebhook =
data: safeJsonParse(error.response.body as string).data, data: safeJsonParse(error.response.body as string).data,
} }
await saveErrorLog({ await saveErrorLog({
resultId, resultId: result.id,
message: 'Webhook returned an error', message: 'Webhook returned an error',
details: { details: {
request, request,
@ -240,7 +235,7 @@ export const executeWebhook =
} }
console.error(error) console.error(error)
await saveErrorLog({ await saveErrorLog({
resultId, resultId: result.id,
message: 'Webhook failed to execute', message: 'Webhook failed to execute',
details: { details: {
request, request,
@ -258,20 +253,20 @@ const getBodyContent =
) => ) =>
async ({ async ({
body, body,
resultValues, result,
groupId, groupId,
variables, variables,
}: { }: {
body?: string | null body?: string | null
resultValues?: ResultValues result?: ResultInSession
groupId: string groupId: string
variables: Variable[] variables: Variable[]
}): Promise<string | undefined> => { }): Promise<string | undefined> => {
if (!body) return if (!body) return
return body === '{{state}}' return body === '{{state}}'
? JSON.stringify( ? JSON.stringify(
resultValues result
? parseAnswers(typebot, linkedTypebots)(resultValues) ? parseAnswers(typebot, linkedTypebots)(result)
: await parseSampleResult(typebot, linkedTypebots)( : await parseSampleResult(typebot, linkedTypebots)(
groupId, groupId,
variables variables

View File

@ -29,7 +29,7 @@ export const parseSampleResult =
return { return {
message: 'This is a sample result, it has been generated ⬇️', message: 'This is a sample result, it has been generated ⬇️',
'Submitted at': new Date().toISOString(), submittedAt: new Date().toISOString(),
...parseResultSample(linkedInputBlocks, header, variables), ...parseResultSample(linkedInputBlocks, header, variables),
} }
} }

View File

@ -115,7 +115,8 @@ const getLinkedTypebot = async (
state: SessionState, state: SessionState,
typebotId: string typebotId: string
): Promise<TypebotInSession | null> => { ): Promise<TypebotInSession | null> => {
const { typebot, isPreview } = state const { typebot, result } = state
const isPreview = !result.id
if (typebotId === 'current') return typebot if (typebotId === 'current') return typebot
const availableTypebots = const availableTypebots =
'linkedTypebots' in state 'linkedTypebots' in state
@ -123,12 +124,12 @@ const getLinkedTypebot = async (
: [typebot] : [typebot]
const linkedTypebot = const linkedTypebot =
availableTypebots.find(byId(typebotId)) ?? availableTypebots.find(byId(typebotId)) ??
(await fetchTypebot({ isPreview }, typebotId)) (await fetchTypebot(isPreview, typebotId))
return linkedTypebot return linkedTypebot
} }
const fetchTypebot = async ( const fetchTypebot = async (
{ isPreview }: Pick<SessionState, 'isPreview'>, isPreview: boolean,
typebotId: string typebotId: string
): Promise<TypebotInSession | null> => { ): Promise<TypebotInSession | null> => {
if (isPreview) { if (isPreview) {

View File

@ -13,7 +13,7 @@ import {
ChatReply, ChatReply,
chatReplySchema, chatReplySchema,
ChatSession, ChatSession,
Result, ResultInSession,
sendMessageInputSchema, sendMessageInputSchema,
SessionState, SessionState,
StartParams, StartParams,
@ -85,7 +85,11 @@ export const sendMessageProcedure = publicProcedure
}, },
}) })
if (!input && session.state.result?.hasStarted) if (
!input &&
session.state.result.answers.length > 0 &&
session.state.result.id
)
await setResultAsCompleted(session.state.result.id) await setResultAsCompleted(session.state.result.id)
return { return {
@ -106,9 +110,6 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
message: 'No typebot provided in startParams', message: 'No typebot provided in startParams',
}) })
const isPreview =
startParams?.isPreview || typeof startParams?.typebot !== 'string'
const typebot = await getTypebot(startParams, userId) const typebot = await getTypebot(startParams, userId)
const prefilledVariables = startParams.prefilledVariables const prefilledVariables = startParams.prefilledVariables
@ -117,8 +118,8 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
const result = await getResult({ const result = await getResult({
...startParams, ...startParams,
isPreview, isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
typebot: typebot.id, typebotId: typebot.id,
prefilledVariables, prefilledVariables,
isNewResultOnRefreshEnabled: isNewResultOnRefreshEnabled:
typebot.settings.general.isNewResultOnRefreshEnabled ?? false, typebot.settings.general.isNewResultOnRefreshEnabled ?? false,
@ -140,10 +141,11 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
typebots: [], typebots: [],
queue: [], queue: [],
}, },
result: result result: {
? { id: result.id, variables: result.variables, hasStarted: false } id: result?.id,
: undefined, variables: result?.variables ?? [],
isPreview, answers: result?.answers ?? [],
},
currentTypebotId: typebot.id, currentTypebotId: typebot.id,
dynamicTheme: parseDynamicThemeInState(typebot.theme), dynamicTheme: parseDynamicThemeInState(typebot.theme),
} }
@ -297,20 +299,21 @@ const getTypebot = async (
} }
const getResult = async ({ const getResult = async ({
typebot, typebotId,
isPreview, isPreview,
resultId, resultId,
prefilledVariables, prefilledVariables,
isNewResultOnRefreshEnabled, isNewResultOnRefreshEnabled,
}: Pick<StartParams, 'isPreview' | 'resultId' | 'typebot'> & { }: Pick<StartParams, 'isPreview' | 'resultId'> & {
typebotId: string
prefilledVariables: Variable[] prefilledVariables: Variable[]
isNewResultOnRefreshEnabled: boolean isNewResultOnRefreshEnabled: boolean
}) => { }) => {
if (isPreview || typeof typebot !== 'string') return if (isPreview) return
const select = { const select = {
id: true, id: true,
variables: true, variables: true,
hasStarted: true, answers: { select: { blockId: true, variableId: true, content: true } },
} satisfies Prisma.ResultSelect } satisfies Prisma.ResultSelect
const existingResult = const existingResult =
@ -318,7 +321,7 @@ const getResult = async ({
? ((await prisma.result.findFirst({ ? ((await prisma.result.findFirst({
where: { id: resultId }, where: { id: resultId },
select, select,
})) as Pick<Result, 'id' | 'variables' | 'hasStarted'>) })) as ResultInSession)
: undefined : undefined
if (existingResult) { if (existingResult) {
@ -344,19 +347,19 @@ const getResult = async ({
return { return {
id: existingResult.id, id: existingResult.id,
variables: updatedResult.variables, variables: updatedResult.variables,
hasStarted: existingResult.hasStarted, answers: existingResult.answers,
} }
} else { } else {
return (await prisma.result.create({ return (await prisma.result.create({
data: { data: {
isCompleted: false, isCompleted: false,
typebotId: typebot, typebotId,
variables: prefilledVariables.filter((variable) => variables: prefilledVariables.filter((variable) =>
isDefined(variable.value) isDefined(variable.value)
), ),
}, },
select, select,
})) as Pick<Result, 'id' | 'variables' | 'hasStarted'> })) as ResultInSession
} }
} }

View File

@ -7,6 +7,7 @@ import { validateUrl } from '@/features/blocks/inputs/url/api'
import { parseVariables, updateVariables } from '@/features/variables' import { parseVariables, updateVariables } from '@/features/variables'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Prisma } from 'db'
import got from 'got' import got from 'got'
import { import {
Block, Block,
@ -15,6 +16,7 @@ import {
ChatReply, ChatReply,
InputBlock, InputBlock,
InputBlockType, InputBlockType,
ResultInSession,
SessionState, SessionState,
} from 'models' } from 'models'
import { isInputBlock, isNotDefined } from 'utils' import { isInputBlock, isNotDefined } from 'utils'
@ -86,17 +88,9 @@ const processAndSaveAnswer =
(state: SessionState, block: InputBlock) => (state: SessionState, block: InputBlock) =>
async (reply: string | null): Promise<SessionState> => { async (reply: string | null): Promise<SessionState> => {
if (!reply) return state if (!reply) return state
if (!state.isPreview && state.result) { let newState = await saveAnswer(state, block)(reply)
await saveAnswer(state.result.id, block)(reply) newState = await saveVariableValueIfAny(newState, block)(reply)
if (!state.result.hasStarted) await setResultAsStarted(state.result.id) return newState
}
const newState = await saveVariableValueIfAny(state, block)(reply)
return {
...newState,
result: newState.result
? { ...newState.result, hasStarted: true }
: undefined,
}
} }
const saveVariableValueIfAny = const saveVariableValueIfAny =
@ -115,13 +109,6 @@ const saveVariableValueIfAny =
return newSessionState return newSessionState
} }
const setResultAsStarted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { hasStarted: true },
})
}
export const setResultAsCompleted = async (resultId: string) => { export const setResultAsCompleted = async (resultId: string) => {
await prisma.result.update({ await prisma.result.update({
where: { id: resultId }, where: { id: resultId },
@ -152,31 +139,65 @@ const parseRetryMessage = (
} }
const saveAnswer = const saveAnswer =
(resultId: string, block: InputBlock) => async (reply: string) => { (state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
const resultId = state.result?.id
const answer = { const answer = {
resultId: resultId, resultId,
blockId: block.id, blockId: block.id,
groupId: block.groupId, groupId: block.groupId,
content: reply, content: reply,
variableId: block.options.variableId, variableId: block.options.variableId,
storageUsed: 0, storageUsed: 0,
} }
if (state.result.answers.length === 0 && state.result.id)
await setResultAsStarted(state.result.id)
const newSessionState = setNewAnswerInState(state)({
blockId: block.id,
variableId: block.options.variableId ?? null,
content: reply,
})
if (reply.includes('http') && block.type === InputBlockType.FILE) { if (reply.includes('http') && block.type === InputBlockType.FILE) {
answer.storageUsed = await computeStorageUsed(reply) answer.storageUsed = await computeStorageUsed(reply)
} }
await prisma.answer.upsert({ if (resultId)
where: { await prisma.answer.upsert({
resultId_blockId_groupId: { where: {
resultId, resultId_blockId_groupId: {
groupId: block.groupId, resultId,
blockId: block.id, groupId: block.groupId,
blockId: block.id,
},
}, },
create: answer as Prisma.AnswerUncheckedCreateInput,
update: answer,
})
return newSessionState
}
const setResultAsStarted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { hasStarted: true },
})
}
const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
const newAnswers = state.result.answers
.filter((answer) => answer.blockId !== newAnswer.blockId)
.concat(newAnswer)
return {
...state,
result: {
...state.result,
answers: newAnswers,
}, },
create: answer, } satisfies SessionState
update: answer,
})
} }
const computeStorageUsed = async (reply: string) => { const computeStorageUsed = async (reply: string) => {

View File

@ -113,7 +113,7 @@ export const executeGroup =
} }
const computeRuntimeOptions = const computeRuntimeOptions =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) => (state: Pick<SessionState, 'result' | 'typebot'>) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => { (block: InputBlock): Promise<RuntimeOptions> | undefined => {
switch (block.type) { switch (block.type) {
case InputBlockType.PAYMENT: { case InputBlockType.PAYMENT: {
@ -158,7 +158,7 @@ const parseBubbleBlock =
} }
const injectVariablesValueInBlock = const injectVariablesValueInBlock =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) => (state: Pick<SessionState, 'result' | 'typebot'>) =>
async (block: InputBlock): Promise<ChatReply['input']> => { async (block: InputBlock): Promise<ChatReply['input']> => {
switch (block.type) { switch (block.type) {
case InputBlockType.CHOICE: { case InputBlockType.CHOICE: {

View File

@ -1 +0,0 @@
export * from './utils'

View File

@ -1,8 +0,0 @@
import prisma from '@/lib/prisma'
import { ResultValues } from 'models'
export const getResultValues = async (resultId: string) =>
(await prisma.result.findUnique({
where: { id: resultId },
include: { answers: true },
})) as ResultValues | null

View File

@ -1 +0,0 @@
export * from './getResultValues'

View File

@ -157,12 +157,10 @@ export const updateVariables =
...state.typebot, ...state.typebot,
variables: updateTypebotVariables(state)(newVariables), variables: updateTypebotVariables(state)(newVariables),
}, },
result: state.result result: {
? { ...state.result,
...state.result, variables: await updateResultVariables(state)(newVariables),
variables: await updateResultVariables(state)(newVariables), },
}
: undefined,
}) })
const updateResultVariables = const updateResultVariables =
@ -170,7 +168,6 @@ const updateResultVariables =
async ( async (
newVariables: VariableWithUnknowValue[] newVariables: VariableWithUnknowValue[]
): Promise<VariableWithValue[]> => { ): Promise<VariableWithValue[]> => {
if (!result) return []
const serializedNewVariables = newVariables.map((variable) => ({ const serializedNewVariables = newVariables.map((variable) => ({
...variable, ...variable,
value: Array.isArray(variable.value) value: Array.isArray(variable.value)
@ -187,14 +184,15 @@ const updateResultVariables =
...serializedNewVariables, ...serializedNewVariables,
].filter((variable) => isDefined(variable.value)) as VariableWithValue[] ].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
await prisma.result.update({ if (result.id)
where: { await prisma.result.update({
id: result.id, where: {
}, id: result.id,
data: { },
variables: updatedVariables, data: {
}, variables: updatedVariables,
}) },
})
return updatedVariables return updatedVariables
} }

View File

@ -1,7 +1,7 @@
import { safeStringify } from '@/features/variables' import { safeStringify } from '@/features/variables'
import { import {
AnswerInput, AnswerInput,
ResultValues, ResultValuesInput,
Variable, Variable,
VariableWithUnknowValue, VariableWithUnknowValue,
VariableWithValue, VariableWithValue,
@ -11,7 +11,7 @@ import { isDefined } from 'utils'
const answersContext = createContext<{ const answersContext = createContext<{
resultId?: string resultId?: string
resultValues: ResultValues resultValues: ResultValuesInput
addAnswer: ( addAnswer: (
existingVariables: Variable[] existingVariables: Variable[]
) => ( ) => (
@ -35,7 +35,7 @@ export const AnswersProvider = ({
onVariablesUpdated?: (variables: VariableWithValue[]) => void onVariablesUpdated?: (variables: VariableWithValue[]) => void
children: ReactNode children: ReactNode
}) => { }) => {
const [resultValues, setResultValues] = useState<ResultValues>({ const [resultValues, setResultValues] = useState<ResultValuesInput>({
answers: [], answers: [],
variables: [], variables: [],
createdAt: new Date(), createdAt: new Date(),

View File

@ -3,7 +3,7 @@ import {
Edge, Edge,
Group, Group,
PublicTypebot, PublicTypebot,
ResultValues, ResultValuesInput,
Typebot, Typebot,
Variable, Variable,
VariableWithUnknowValue, VariableWithUnknowValue,
@ -45,7 +45,7 @@ export type IntegrationState = {
blockId: string blockId: string
isPreview: boolean isPreview: boolean
variables: Variable[] variables: Variable[]
resultValues: ResultValues resultValues: ResultValuesInput
groups: Group[] groups: Group[]
resultId?: string resultId?: string
parentTypebotIds: string[] parentTypebotIds: string[]

View File

@ -6,8 +6,6 @@ import {
redirectOptionsSchema, redirectOptionsSchema,
} from './blocks' } from './blocks'
import { publicTypebotSchema } from './publicTypebot' import { publicTypebotSchema } from './publicTypebot'
import { ChatSession as ChatSessionPrisma } from 'db'
import { schemaForType } from './utils'
import { logSchema, resultSchema } from './result' import { logSchema, resultSchema } from './result'
import { typebotSchema } from './typebot' import { typebotSchema } from './typebot'
import { import {
@ -18,6 +16,7 @@ import {
audioBubbleContentSchema, audioBubbleContentSchema,
embedBubbleContentSchema, embedBubbleContentSchema,
} from './blocks/bubbles' } from './blocks/bubbles'
import { answerSchema } from './answer'
const typebotInSessionStateSchema = publicTypebotSchema.pick({ const typebotInSessionStateSchema = publicTypebotSchema.pick({
id: true, id: true,
@ -31,6 +30,23 @@ const dynamicThemeSchema = z.object({
guestAvatarUrl: z.string().optional(), guestAvatarUrl: z.string().optional(),
}) })
const answerInSessionStateSchema = answerSchema.pick({
content: true,
blockId: true,
variableId: true,
})
const resultInSessionStateSchema = resultSchema
.pick({
variables: true,
})
.and(
z.object({
answers: z.array(answerInSessionStateSchema),
id: z.string().optional(),
})
)
export const sessionStateSchema = z.object({ export const sessionStateSchema = z.object({
typebot: typebotInSessionStateSchema, typebot: typebotInSessionStateSchema,
dynamicTheme: dynamicThemeSchema.optional(), dynamicTheme: dynamicThemeSchema.optional(),
@ -39,10 +55,7 @@ export const sessionStateSchema = z.object({
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })), queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
}), }),
currentTypebotId: z.string(), currentTypebotId: z.string(),
result: resultSchema result: resultInSessionStateSchema,
.pick({ id: true, variables: true, hasStarted: true })
.optional(),
isPreview: z.boolean(),
currentBlock: z currentBlock: z
.object({ .object({
blockId: z.string(), blockId: z.string(),
@ -51,14 +64,12 @@ export const sessionStateSchema = z.object({
.optional(), .optional(),
}) })
const chatSessionSchema = schemaForType<ChatSessionPrisma>()( const chatSessionSchema = z.object({
z.object({ id: z.string(),
id: z.string(), createdAt: z.date(),
createdAt: z.date(), updatedAt: z.date(),
updatedAt: z.date(), state: sessionStateSchema,
state: sessionStateSchema, })
})
)
const textMessageSchema = z.object({ const textMessageSchema = z.object({
type: z.enum([BubbleBlockType.TEXT]), type: z.enum([BubbleBlockType.TEXT]),
@ -234,6 +245,7 @@ export const chatReplySchema = z.object({
export type ChatSession = z.infer<typeof chatSessionSchema> export type ChatSession = z.infer<typeof chatSessionSchema>
export type SessionState = z.infer<typeof sessionStateSchema> export type SessionState = z.infer<typeof sessionStateSchema>
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema> export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
export type ResultInSession = z.infer<typeof resultInSessionStateSchema>
export type ChatReply = z.infer<typeof chatReplySchema> export type ChatReply = z.infer<typeof chatReplySchema>
export type ChatMessage = z.infer<typeof chatMessageSchema> export type ChatMessage = z.infer<typeof chatMessageSchema>
export type SendMessageInput = z.infer<typeof sendMessageInputSchema> export type SendMessageInput = z.infer<typeof sendMessageInputSchema>

View File

@ -48,11 +48,16 @@ export type ResultWithAnswersInput = z.infer<
> >
export type Log = z.infer<typeof logSchema> export type Log = z.infer<typeof logSchema>
export type ResultValues = Pick< export type ResultValuesInput = Pick<
ResultWithAnswersInput, ResultWithAnswersInput,
'answers' | 'createdAt' | 'variables' 'answers' | 'createdAt' | 'variables'
> >
export type ResultValues = Pick<
ResultWithAnswers,
'answers' | 'createdAt' | 'variables'
>
export type ResultHeaderCell = { export type ResultHeaderCell = {
id: string id: string
label: string label: string

View File

@ -6,7 +6,6 @@ const generalSettings = z.object({
isInputPrefillEnabled: z.boolean().optional(), isInputPrefillEnabled: z.boolean().optional(),
isHideQueryParamsEnabled: z.boolean().optional(), isHideQueryParamsEnabled: z.boolean().optional(),
isNewResultOnRefreshEnabled: z.boolean().optional(), isNewResultOnRefreshEnabled: z.boolean().optional(),
isResultSavingEnabled: z.boolean().optional(),
}) })
const typingEmulation = z.object({ const typingEmulation = z.object({
@ -36,7 +35,6 @@ export const defaultSettings: Settings = {
isNewResultOnRefreshEnabled: true, isNewResultOnRefreshEnabled: true,
isInputPrefillEnabled: true, isInputPrefillEnabled: true,
isHideQueryParamsEnabled: true, isHideQueryParamsEnabled: true,
isResultSavingEnabled: true,
}, },
typingEmulation: { enabled: true, speed: 300, maxDelay: 1.5 }, typingEmulation: { enabled: true, speed: 300, maxDelay: 1.5 },
metadata: { metadata: {

View File

@ -6,9 +6,9 @@ import {
Answer, Answer,
VariableWithValue, VariableWithValue,
Typebot, Typebot,
ResultWithAnswersInput,
ResultWithAnswers, ResultWithAnswers,
InputBlockType, InputBlockType,
ResultInSession,
} from 'models' } from 'models'
import { isInputBlock, isDefined, byId, isNotEmpty } from './utils' import { isInputBlock, isDefined, byId, isNotEmpty } from './utils'
@ -218,16 +218,16 @@ export const parseAnswers =
createdAt, createdAt,
answers, answers,
variables: resultVariables, variables: resultVariables,
}: Pick<ResultWithAnswersInput, 'answers' | 'variables'> & { }: Omit<ResultInSession, 'hasStarted'> & { createdAt?: Date | string }): {
// TODO: remove once we are using 100% tRPC
createdAt: Date | string
}): {
[key: string]: string [key: string]: string
} => { } => {
const header = parseResultHeader(typebot, linkedTypebots) const header = parseResultHeader(typebot, linkedTypebots)
return { return {
submittedAt: submittedAt: !createdAt
typeof createdAt === 'string' ? createdAt : createdAt.toISOString(), ? new Date().toISOString()
: typeof createdAt === 'string'
? createdAt
: createdAt.toISOString(),
...[...answers, ...resultVariables].reduce<{ ...[...answers, ...resultVariables].reduce<{
[key: string]: string [key: string]: string
}>((o, answerOrVariable) => { }>((o, answerOrVariable) => {