Skip to content

Commit

Permalink
Feature: Collect contact information from users inside the chatbot (F…
Browse files Browse the repository at this point in the history
…lowiseAI#1948)

* Add leads settings to chatflow configuration

* Add leads tab to chatflow configuration with options for lead capture

* Add database entity and migrations for leads

* Add endpoint for adding and fetching leads

* Show lead capture form in UI chat window when enabled

* Add view leads dialog

* Make export leads functional

* Add input for configuring message on successful lead capture

* Add migrations for adding lead email in chat message if available

* show lead email in view messages

* ui touch up

* Remove unused code and update how lead email is shown in view messages dialog

* Fix lead not getting saved

* Disable input when lead form is shown and save lead info to localstorage

* Fix lead capture form not working

* disabled lead save button until at least one form field is turned on, get rid of local storage _LEAD

* add leads API to as whitelist public endpoint

* Send leadEmail in internal chat inputs

* Fix condition for disabling input field and related buttons when lead is enabled/disabled and when lead is saved

* update leads ui

* update error message and alter table add column sqlite migration

---------

Co-authored-by: Henry <[email protected]>
  • Loading branch information
0xi4o and HenryHengZJ authored May 2, 2024
1 parent adea2f0 commit db452cd
Show file tree
Hide file tree
Showing 31 changed files with 979 additions and 57 deletions.
12 changes: 12 additions & 0 deletions packages/server/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface IChatMessage {
memoryType?: string
sessionId?: string
createdDate: Date
leadEmail?: string
}

export interface IChatMessageFeedback {
Expand Down Expand Up @@ -93,6 +94,16 @@ export interface IVariable {
createdDate: Date
}

export interface ILead {
id: string
name?: string
email?: string
phone?: string
chatflowid: string
chatId: string
createdDate: Date
}

export interface IUpsertHistory {
id: string
chatflowid: string
Expand Down Expand Up @@ -200,6 +211,7 @@ export interface IncomingInput {
chatId?: string
stopNodeId?: string
uploads?: IFileUpload[]
leadEmail?: string
}

export interface IActiveChatflows {
Expand Down
40 changes: 40 additions & 0 deletions packages/server/src/controllers/leads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from 'express'
import leadsService from '../../services/leads'
import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'

const getAllLeadsForChatflow = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params.id === 'undefined' || req.params.id === '') {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: leadsController.getAllLeadsForChatflow - id not provided!`
)
}
const chatflowid = req.params.id
const apiResponse = await leadsService.getAllLeads(chatflowid)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}

const createLeadInChatflow = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.body === 'undefined' || req.body === '') {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: leadsController.createLeadInChatflow - body not provided!`
)
}
const apiResponse = await leadsService.createLead(req.body)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}

export default {
createLeadInChatflow,
getAllLeadsForChatflow
}
3 changes: 3 additions & 0 deletions packages/server/src/database/entities/ChatMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ export class ChatMessage implements IChatMessage {
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date

@Column({ nullable: true, type: 'text' })
leadEmail?: string
}
27 changes: 27 additions & 0 deletions packages/server/src/database/entities/Lead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable */
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { ILead } from '../../Interface'

@Entity()
export class Lead implements ILead {
@PrimaryGeneratedColumn('uuid')
id: string

@Column()
name?: string

@Column()
email?: string

@Column()
phone?: string

@Column()
chatflowid: string

@Column()
chatId: string

@CreateDateColumn()
createdDate: Date
}
2 changes: 2 additions & 0 deletions packages/server/src/database/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Credential } from './Credential'
import { Tool } from './Tool'
import { Assistant } from './Assistant'
import { Variable } from './Variable'
import { Lead } from './Lead'
import { UpsertHistory } from './UpsertHistory'

export const entities = {
Expand All @@ -15,5 +16,6 @@ export const entities = {
Tool,
Assistant,
Variable,
Lead,
UpsertHistory
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLead1710832127079 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`lead\` (
\`id\` varchar(36) NOT NULL,
\`chatflowid\` varchar(255) NOT NULL,
\`chatId\` varchar(255) NOT NULL,
\`name\` text,
\`email\` text,
\`phone\` text,
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE lead`)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLeadToChatMessage1711538023578 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const columnExists = await queryRunner.hasColumn('chat_message', 'leadEmail')
if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`leadEmail\` TEXT;`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`leadEmail\`;`)
}
}
6 changes: 5 additions & 1 deletion packages/server/src/database/migrations/mysql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntit
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213626553 } from './1707213626553-AddFeedback'
import { AddLead1710832127079 } from './1710832127079-AddLead'
import { AddLeadToChatMessage1711538023578 } from './1711538023578-AddLeadToChatMessage'

export const mysqlMigrations = [
Init1693840429259,
Expand All @@ -33,5 +35,7 @@ export const mysqlMigrations = [
AddVariableEntity1699325775451,
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213626553
AddFeedback1707213626553,
AddLead1710832127079,
AddLeadToChatMessage1711538023578
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLead1710832137905 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS lead (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"chatflowid" varchar NOT NULL,
"chatId" varchar NOT NULL,
"name" text,
"email" text,
"phone" text,
"createdDate" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "PK_98419043dd704f54-9830ab78f0" PRIMARY KEY (id)
);`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE lead`)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLeadToChatMessage1711538016098 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "leadEmail" TEXT;`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "leadEmail";`)
}
}
6 changes: 5 additions & 1 deletion packages/server/src/database/migrations/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213601923 } from './1707213601923-AddFeedback'
import { FieldTypes1710497452584 } from './1710497452584-FieldTypes'
import { AddLead1710832137905 } from './1710832137905-AddLead'
import { AddLeadToChatMessage1711538016098 } from './1711538016098-AddLeadToChatMessage'

export const postgresMigrations = [
Init1693891895163,
Expand All @@ -35,5 +37,7 @@ export const postgresMigrations = [
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213601923,
FieldTypes1710497452584
FieldTypes1710497452584,
AddLead1710832137905,
AddLeadToChatMessage1711538016098
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLead1710832117612 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS "lead" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "chatId" varchar NOT NULL, "name" text, "email" text, "phone" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')));`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "lead";`)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddLeadToChatMessage1711537986113 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "leadEmail" TEXT;`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "leadEmail";`)
}
}
6 changes: 5 additions & 1 deletion packages/server/src/database/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntit
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213619308 } from './1707213619308-AddFeedback'
import { AddLead1710832117612 } from './1710832117612-AddLead'
import { AddLeadToChatMessage1711537986113 } from './1711537986113-AddLeadToChatMessage'

export const sqliteMigrations = [
Init1693835579790,
Expand All @@ -33,5 +35,7 @@ export const sqliteMigrations = [
AddVariableEntity1699325775451,
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213619308
AddFeedback1707213619308,
AddLead1710832117612,
AddLeadToChatMessage1711537986113
]
1 change: 1 addition & 0 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export class App {
'/api/v1/chatflows-uploads',
'/api/v1/openai-assistants-file/download',
'/api/v1/feedback',
'/api/v1/leads',
'/api/v1/get-upload-file',
'/api/v1/ip'
]
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import internalChatmessagesRouter from './internal-chat-messages'
import internalPredictionRouter from './internal-predictions'
import getUploadFileRouter from './get-upload-file'
import getUploadPathRouter from './get-upload-path'
import leadsRouter from './leads'
import loadPromptRouter from './load-prompts'
import marketplacesRouter from './marketplaces'
import nodeConfigRouter from './node-configs'
Expand Down Expand Up @@ -55,6 +56,7 @@ router.use('/internal-chatmessage', internalChatmessagesRouter)
router.use('/internal-prediction', internalPredictionRouter)
router.use('/get-upload-file', getUploadFileRouter)
router.use('/get-upload-path', getUploadPathRouter)
router.use('/leads', leadsRouter)
router.use('/load-prompt', loadPromptRouter)
router.use('/marketplaces', marketplacesRouter)
router.use('/node-config', nodeConfigRouter)
Expand Down
11 changes: 11 additions & 0 deletions packages/server/src/routes/leads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from 'express'
import leadsController from '../../controllers/leads'
const router = express.Router()

// CREATE
router.post('/', leadsController.createLeadInChatflow)

// READ
router.get(['/', '/:id'], leadsController.getAllLeadsForChatflow)

export default router
43 changes: 43 additions & 0 deletions packages/server/src/services/leads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { v4 as uuidv4 } from 'uuid'
import { StatusCodes } from 'http-status-codes'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Lead } from '../../database/entities/Lead'
import { ILead } from '../../Interface'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'

const getAllLeads = async (chatflowid: string) => {
try {
const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(Lead).find({
where: {
chatflowid
}
})
return dbResponse
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: leadsService.getAllLeads - ${getErrorMessage(error)}`)
}
}

const createLead = async (body: Partial<ILead>) => {
try {
const chatId = body.chatId ?? uuidv4()

const newLead = new Lead()
Object.assign(newLead, body)
Object.assign(newLead, { chatId })

const appServer = getRunningExpressApp()
const lead = appServer.AppDataSource.getRepository(Lead).create(newLead)
const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)
return dbResponse
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: leadsService.createLead - ${getErrorMessage(error)}`)
}
}

export default {
createLead,
getAllLeads
}
3 changes: 2 additions & 1 deletion packages/server/src/utils/buildChatflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInter
memoryType,
sessionId,
createdDate: userMessageDateTime,
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined,
leadEmail: incomingInput.leadEmail
}
await utilAddChatMessage(userMessage)

Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/api/lead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import client from './client'

const getLeads = (id) => client.get(`/leads/${id}`)
const addLead = (body) => client.post(`/leads/`, body)

export default {
getLeads,
addLead
}
1 change: 1 addition & 0 deletions packages/ui/src/assets/images/leads_empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit db452cd

Please sign in to comment.