diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 106cf27a5..90f2623f8 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -42,6 +42,7 @@ export interface IChatMessage { memoryType?: string sessionId?: string createdDate: Date + leadEmail?: string } export interface IChatMessageFeedback { @@ -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 @@ -200,6 +211,7 @@ export interface IncomingInput { chatId?: string stopNodeId?: string uploads?: IFileUpload[] + leadEmail?: string } export interface IActiveChatflows { diff --git a/packages/server/src/controllers/leads/index.ts b/packages/server/src/controllers/leads/index.ts new file mode 100644 index 000000000..db403a02a --- /dev/null +++ b/packages/server/src/controllers/leads/index.ts @@ -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 +} diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index f0dcd0c5b..4cc84983f 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -44,4 +44,7 @@ export class ChatMessage implements IChatMessage { @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date + + @Column({ nullable: true, type: 'text' }) + leadEmail?: string } diff --git a/packages/server/src/database/entities/Lead.ts b/packages/server/src/database/entities/Lead.ts new file mode 100644 index 000000000..fc7a1e9d8 --- /dev/null +++ b/packages/server/src/database/entities/Lead.ts @@ -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 +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index d9b9536ea..8dc6f86a1 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -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 = { @@ -15,5 +16,6 @@ export const entities = { Tool, Assistant, Variable, + Lead, UpsertHistory } diff --git a/packages/server/src/database/migrations/mysql/1710832127079-AddLead.ts b/packages/server/src/database/migrations/mysql/1710832127079-AddLead.ts new file mode 100644 index 000000000..58edde091 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1710832127079-AddLead.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLead1710832127079 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE lead`) + } +} diff --git a/packages/server/src/database/migrations/mysql/1711538023578-AddLeadToChatMessage.ts b/packages/server/src/database/migrations/mysql/1711538023578-AddLeadToChatMessage.ts new file mode 100644 index 000000000..e1fff8fc6 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1711538023578-AddLeadToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLeadToChatMessage1711538023578 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`leadEmail\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index b01024221..a2fc0dd5f 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -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, @@ -33,5 +35,7 @@ export const mysqlMigrations = [ AddVariableEntity1699325775451, AddSpeechToText1706364937060, AddUpsertHistoryEntity1709814301358, - AddFeedback1707213626553 + AddFeedback1707213626553, + AddLead1710832127079, + AddLeadToChatMessage1711538023578 ] diff --git a/packages/server/src/database/migrations/postgres/1710832137905-AddLead.ts b/packages/server/src/database/migrations/postgres/1710832137905-AddLead.ts new file mode 100644 index 000000000..006107619 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1710832137905-AddLead.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLead1710832137905 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE lead`) + } +} diff --git a/packages/server/src/database/migrations/postgres/1711538016098-AddLeadToChatMessage.ts b/packages/server/src/database/migrations/postgres/1711538016098-AddLeadToChatMessage.ts new file mode 100644 index 000000000..09924d0e6 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1711538016098-AddLeadToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLeadToChatMessage1711538016098 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "leadEmail" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "leadEmail";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 3f37c6e8b..5b7b8cb1f 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -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, @@ -35,5 +37,7 @@ export const postgresMigrations = [ AddSpeechToText1706364937060, AddUpsertHistoryEntity1709814301358, AddFeedback1707213601923, - FieldTypes1710497452584 + FieldTypes1710497452584, + AddLead1710832137905, + AddLeadToChatMessage1711538016098 ] diff --git a/packages/server/src/database/migrations/sqlite/1710832117612-AddLead.ts b/packages/server/src/database/migrations/sqlite/1710832117612-AddLead.ts new file mode 100644 index 000000000..57eccc90f --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1710832117612-AddLead.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLead1710832117612 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE IF EXISTS "lead";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/1711537986113-AddLeadToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1711537986113-AddLeadToChatMessage.ts new file mode 100644 index 000000000..38218fa32 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1711537986113-AddLeadToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddLeadToChatMessage1711537986113 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "leadEmail" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "leadEmail";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 0cc692dd6..91b7b289f 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -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, @@ -33,5 +35,7 @@ export const sqliteMigrations = [ AddVariableEntity1699325775451, AddSpeechToText1706364937060, AddUpsertHistoryEntity1709814301358, - AddFeedback1707213619308 + AddFeedback1707213619308, + AddLead1710832117612, + AddLeadToChatMessage1711537986113 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 42b1947fc..5a1da7fa6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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' ] diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3382dba82..8b6d1816d 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -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' @@ -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) diff --git a/packages/server/src/routes/leads/index.ts b/packages/server/src/routes/leads/index.ts new file mode 100644 index 000000000..64209eefd --- /dev/null +++ b/packages/server/src/routes/leads/index.ts @@ -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 diff --git a/packages/server/src/services/leads/index.ts b/packages/server/src/services/leads/index.ts new file mode 100644 index 000000000..85e38d386 --- /dev/null +++ b/packages/server/src/services/leads/index.ts @@ -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) => { + 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 +} diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 71e393aef..3ffefff32 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -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) diff --git a/packages/ui/src/api/lead.js b/packages/ui/src/api/lead.js new file mode 100644 index 000000000..d1673e9d0 --- /dev/null +++ b/packages/ui/src/api/lead.js @@ -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 +} diff --git a/packages/ui/src/assets/images/leads_empty.svg b/packages/ui/src/assets/images/leads_empty.svg new file mode 100644 index 000000000..0cafb1a49 --- /dev/null +++ b/packages/ui/src/assets/images/leads_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index c45d7a20c..fecda1ee8 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -6,7 +6,8 @@ import { IconCopy, IconMessage, IconDatabaseExport, - IconAdjustmentsHorizontal + IconAdjustmentsHorizontal, + IconUsers } from '@tabler/icons' // constant @@ -17,7 +18,8 @@ const icons = { IconCopy, IconMessage, IconDatabaseExport, - IconAdjustmentsHorizontal + IconAdjustmentsHorizontal, + IconUsers } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -34,6 +36,13 @@ const settings = { url: '', icon: icons.IconMessage }, + { + id: 'viewLeads', + title: 'View Leads', + type: 'item', + url: '', + icon: icons.IconUsers + }, { id: 'viewUpsertHistory', title: 'Upsert History', diff --git a/packages/ui/src/ui-component/cards/StatsCard.jsx b/packages/ui/src/ui-component/cards/StatsCard.jsx index 57e8205df..9e24d353a 100644 --- a/packages/ui/src/ui-component/cards/StatsCard.jsx +++ b/packages/ui/src/ui-component/cards/StatsCard.jsx @@ -10,10 +10,10 @@ const StatsCard = ({ title, stat }) => { return ( - + {title} - + {stat} diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx index 5fde038ab..8743abcdf 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -2,12 +2,14 @@ import PropTypes from 'prop-types' import { useState } from 'react' import { createPortal } from 'react-dom' import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material' +import { tabsClasses } from '@mui/material/Tabs' import SpeechToText from '@/ui-component/extended/SpeechToText' import RateLimit from '@/ui-component/extended/RateLimit' import AllowedDomains from '@/ui-component/extended/AllowedDomains' import ChatFeedback from '@/ui-component/extended/ChatFeedback' import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import StarterPrompts from '@/ui-component/extended/StarterPrompts' +import Leads from '@/ui-component/extended/Leads' const CHATFLOW_CONFIGURATION_TABS = [ { @@ -33,6 +35,10 @@ const CHATFLOW_CONFIGURATION_TABS = [ { label: 'Analyse Chatflow', id: 'analyseChatflow' + }, + { + label: 'Leads', + id: 'leads' } ] @@ -83,10 +89,19 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { setTabValue(value)} aria-label='tabs' + variant='scrollable' + scrollButtons='auto' > {CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( { {item.id === 'chatFeedback' ? : null} {item.id === 'allowedDomains' ? : null} {item.id === 'analyseChatflow' ? : null} + {item.id === 'leads' ? : null} ))} diff --git a/packages/ui/src/ui-component/dialog/ViewLeadsDialog.jsx b/packages/ui/src/ui-component/dialog/ViewLeadsDialog.jsx new file mode 100644 index 000000000..42c9ade3a --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ViewLeadsDialog.jsx @@ -0,0 +1,208 @@ +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect, forwardRef } from 'react' +import PropTypes from 'prop-types' +import moment from 'moment' + +// material-ui +import { + Button, + ListItemButton, + Dialog, + DialogContent, + DialogTitle, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Stack, + Box, + OutlinedInput +} from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { IconFileExport, IconSearch } from '@tabler/icons' +import leadsEmptySVG from '@/assets/images/leads_empty.svg' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' + +// API +import useApi from '@/hooks/useApi' +import leadsApi from '@/api/lead' + +import '@/views/chatmessage/ChatMessage.css' +import 'react-datepicker/dist/react-datepicker.css' + +const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { + return ( + + {value} + + ) +}) + +DatePickerCustomInput.propTypes = { + value: PropTypes.string, + onClick: PropTypes.func +} + +const ViewLeadsDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + const theme = useTheme() + + const [leads, setLeads] = useState([]) + const [search, setSearch] = useState('') + const getLeadsApi = useApi(leadsApi.getLeads) + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterLeads(data) { + return ( + data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || + (data.email && data.email.toLowerCase().indexOf(search.toLowerCase()) > -1) || + (data.phone && data.phone.toLowerCase().indexOf(search.toLowerCase()) > -1) + ) + } + + const exportMessages = async () => { + const exportData = { + leads + } + const dataStr = JSON.stringify(exportData, null, 2) + const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + + const exportFileDefaultName = `${dialogProps.chatflow.id}-leads.json` + + let linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } + + useEffect(() => { + if (getLeadsApi.data) { + setLeads(getLeadsApi.data) + } + }, [getLeadsApi.data]) + + useEffect(() => { + if (dialogProps.chatflow) { + getLeadsApi.request(dialogProps.chatflow.id) + } + + return () => { + setLeads([]) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + +
+ {dialogProps.title} + + + + } + type='search' + /> +
+ {leads && leads.length > 0 && ( + + )} +
+ + + {leads && leads.length == 0 && ( + + + msgEmptySVG + +
No Leads
+
+ )} + {leads && leads.length > 0 && ( + + + + + Name + Email Address + Phone + Created Date + + + + {leads.filter(filterLeads).map((lead, index) => ( + + {lead.name} + {lead.email} + {lead.phone} + {moment(lead.createdDate).format('MMMM Do, YYYY')} + + ))} + +
+
+ )} +
+
+ ) : null + + return createPortal(component, portalElement) +} + +ViewLeadsDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default ViewLeadsDialog diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index ef0ad1a41..dd2858f20 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -102,6 +102,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [chatTypeFilter, setChatTypeFilter] = useState([]) const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) const [endDate, setEndDate] = useState(new Date()) + const [leadEmail, setLeadEmail] = useState('') const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) @@ -191,6 +192,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { source: chatmsg.chatType === 'INTERNAL' ? 'UI' : 'API/Embed', sessionId: chatmsg.sessionId ?? null, memoryType: chatmsg.memoryType ?? null, + email: leadEmail ?? null, messages: [msg] } } else if (Object.prototype.hasOwnProperty.call(obj, chatPK)) { @@ -407,6 +409,13 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setSourceDialogOpen(true) } + useEffect(() => { + const leadEmailFromChatMessages = chatMessages.filter((message) => message.type === 'userMessage' && message.leadEmail) + if (leadEmailFromChatMessages.length) { + setLeadEmail(leadEmailFromChatMessages[0].leadEmail) + } + }, [chatMessages, selectedMessageIndex]) + useEffect(() => { if (getChatmessageFromPKApi.data) { getChatMessages(getChatmessageFromPKApi.data) @@ -450,6 +459,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setStartDate(new Date().setMonth(new Date().getMonth() - 1)) setEndDate(new Date()) setStats([]) + setLeadEmail('') } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -639,6 +649,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { Memory: {chatMessages[1].memoryType} )} + {leadEmail && ( +
+ Email: {leadEmail} +
+ )}
{ )}
{ + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [leadsConfig, setLeadsConfig] = useState({}) + const [chatbotConfig, setChatbotConfig] = useState({}) + + const handleChange = (key, value) => { + setLeadsConfig({ + ...leadsConfig, + [key]: value + }) + } + + const onSave = async () => { + try { + let value = { + leads: leadsConfig + } + chatbotConfig.leads = value.leads + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Leads configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Leads configuration: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.leads) { + setLeadsConfig(chatbotConfig.leads) + } + } + + return () => {} + }, [dialogProps]) + + return ( + <> + + handleChange('status', value)} value={leadsConfig.status} /> + {leadsConfig && leadsConfig['status'] && ( + <> + + Form Title + { + handleChange('title', e.target.value) + }} + /> + + + Message after lead captured + { + handleChange('successMessage', e.target.value) + }} + /> + + Form fields + + + handleChange('name', value)} value={leadsConfig.name} /> + handleChange('email', value)} + value={leadsConfig.email} + /> + handleChange('phone', value)} value={leadsConfig.phone} /> + + + + )} + + + Save + + + ) +} + +Leads.propTypes = { + dialogProps: PropTypes.object +} + +export default Leads diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 22799659a..71e2d6a5d 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -519,9 +519,9 @@ export const formatDataGridRows = (rows) => { } } -export const setLocalStorageChatflow = (chatflowid, chatId) => { +export const setLocalStorageChatflow = (chatflowid, chatId, saveObj = {}) => { const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) - const obj = {} + const obj = { ...saveObj } if (chatId) obj.chatId = chatId if (!chatDetails) { @@ -538,6 +538,34 @@ export const setLocalStorageChatflow = (chatflowid, chatId) => { } } +export const getLocalStorageChatflow = (chatflowid) => { + const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) + if (!chatDetails) return {} + try { + return JSON.parse(chatDetails) + } catch (e) { + return {} + } +} + +export const removeLocalStorageChatHistory = (chatflowid) => { + const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) + if (!chatDetails) return + try { + const parsedChatDetails = JSON.parse(chatDetails) + if (parsedChatDetails.lead) { + // Dont remove lead when chat is cleared + const obj = { lead: parsedChatDetails.lead } + localStorage.removeItem(`${chatflowid}_INTERNAL`) + localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify(obj)) + } else { + localStorage.removeItem(`${chatflowid}_INTERNAL`) + } + } catch (e) { + return + } +} + export const unshiftFiles = (configData) => { const filesConfig = configData.find((config) => config.name === 'files') if (filesConfig) { diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index 130f816e3..88afed789 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -28,6 +28,7 @@ import useApi from '@/hooks/useApi' import { generateExportFlowData } from '@/utils/genericHelper' import { uiBaseURL } from '@/store/constant' import { SET_CHATFLOW } from '@/store/actions' +import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog' // ==============================|| CANVAS HEADER ||============================== // @@ -46,6 +47,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) + const [viewLeadsDialogOpen, setViewLeadsDialogOpen] = useState(false) + const [viewLeadsDialogProps, setViewLeadsDialogProps] = useState({}) const [upsertHistoryDialogOpen, setUpsertHistoryDialogOpen] = useState(false) const [upsertHistoryDialogProps, setUpsertHistoryDialogProps] = useState({}) const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false) @@ -65,6 +68,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl chatflow: chatflow }) setViewMessagesDialogOpen(true) + } else if (setting === 'viewLeads') { + setViewLeadsDialogProps({ + title: 'View Leads', + chatflow: chatflow + }) + setViewLeadsDialogOpen(true) } else if (setting === 'viewUpsertHistory') { setUpsertHistoryDialogProps({ title: 'View Upsert History', @@ -402,6 +411,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl dialogProps={viewMessagesDialogProps} onCancel={() => setViewMessagesDialogOpen(false)} /> + setViewLeadsDialogOpen(false)} /> 0) params.uploads = urls + if (leadEmail) params.leadEmail = leadEmail if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) @@ -573,6 +585,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews if (config.chatFeedback) { setChatFeedbackStatus(config.chatFeedback.status) } + + if (config.leads) { + setLeadsConfig(config.leads) + if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { + setMessages((prevMessages) => { + const leadCaptureMessage = { + message: '', + type: 'leadCaptureMessage' + } + + return [...prevMessages, leadCaptureMessage] + }) + } + } } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -605,6 +631,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews setIsRecording(false) + // leads + const savedLead = getLocalStorageChatflow(chatflowid)?.lead + if (savedLead) { + setIsLeadSaved(!!savedLead) + setLeadEmail(savedLead.email) + } + // SocketIO socket = socketIOClient(baseURL) @@ -731,6 +764,36 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews } } + const handleLeadCaptureSubmit = async (event) => { + if (event) event.preventDefault() + setIsLeadSaving(true) + + const body = { + chatflowid, + chatId, + name: leadName, + email: leadEmail, + phone: leadPhone + } + + const result = await leadsApi.addLead(body) + if (result.data) { + const data = result.data + if (!chatId) setChatId(data.chatId) + setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }) + setIsLeadSaved(true) + setLeadEmail(leadEmail) + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)] + if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages + allMessages[allMessages.length - 1].message = + leadsConfig.successMessage || 'Thank you for submitting your contact information.' + return allMessages + }) + } + setIsLeadSaving(false) + } + return (
{isDragActive && ( @@ -763,7 +826,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews // The latest message sent by the user will be animated while waiting for a response {/* Display the correct icon depending on the message type */} - {message.type === 'apiMessage' ? ( + {message.type === 'apiMessage' || message.type === 'leadCaptureMessage' ? ( AI ) : ( Me )} -
+
{message.usedTools && ( -
+
{message.usedTools.map((tool, index) => { return ( { return ( <> - {item.mime.startsWith('image/') ? ( + {item?.mime?.startsWith('image/') ? ( )}
- {/* Messages are being rendered in Markdown format */} - + + {leadsConfig.title || 'Let us know where we can reach you:'} + +
+ {leadsConfig.name && ( + setLeadName(e.target.value)} /> - ) : ( - - {children} - - ) - } - }} - > - {message.message} - + )} + {leadsConfig.email && ( + setLeadEmail(e.target.value)} + /> + )} + {leadsConfig.phone && ( + setLeadPhone(e.target.value)} + /> + )} + + + + + + ) : ( + <> + {/* Messages are being rendered in Markdown format */} + + ) : ( + + {children} + + ) + } + }} + > + {message.message} + + + )}
{message.type === 'apiMessage' && message.id && chatFeedbackStatus ? ( <> - + copyMessageToClipboard(message.message)} /> {!message.feedback || message.feedback.rating === '' || @@ -901,11 +1065,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews ) : null} {message.fileAnnotations && ( -
+
{message.fileAnnotations.map((fileAnnotation, index) => { return (
)} {message.sourceDocuments && ( -
+
{removeDuplicateURL(message).map((source, index) => { const URL = source.metadata && source.metadata.source @@ -1076,7 +1256,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews // eslint-disable-next-line autoFocus sx={{ width: '100%' }} - disabled={loading || !chatflowid} + disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)} onKeyDown={handleEnter} id='userInput' name='userInput' @@ -1091,11 +1271,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews @@ -1108,20 +1294,28 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews onMicrophonePressed()} type='button' - disabled={loading || !chatflowid} + disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)} edge='end' > )} - + {loading ? (
@@ -1130,7 +1324,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews // Send icon SVG in input field )} diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.jsx b/packages/ui/src/views/chatmessage/ChatPopUp.jsx index d54478db2..6d0d31bcc 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.jsx +++ b/packages/ui/src/views/chatmessage/ChatPopUp.jsx @@ -23,6 +23,9 @@ import useNotifier from '@/utils/useNotifier' // Const import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +// Utils +import { getLocalStorageChatflow, removeLocalStorageChatHistory } from '@/utils/genericHelper' + export const ChatPopUp = ({ chatflowid }) => { const theme = useTheme() const { confirm } = useConfirm() @@ -86,11 +89,10 @@ export const ChatPopUp = ({ chatflowid }) => { if (isConfirmed) { try { - const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) - if (!chatDetails) return - const objChatDetails = JSON.parse(chatDetails) + const objChatDetails = getLocalStorageChatflow(chatflowid) + if (!objChatDetails.chatId) return await chatmessageApi.deleteChatmessage(chatflowid, { chatId: objChatDetails.chatId, chatType: 'INTERNAL' }) - localStorage.removeItem(`${chatflowid}_INTERNAL`) + removeLocalStorageChatHistory(chatflowid) resetChatDialog() enqueueSnackbar({ message: 'Succesfully cleared all chat history',