Feature: Collect contact information from users inside the chatbot (#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 <hzj94@hotmail.com>
This commit is contained in:
Ilango 2024-05-02 06:24:59 +05:30 committed by GitHub
parent adea2f0830
commit db452cd74d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 979 additions and 57 deletions

View File

@ -42,6 +42,7 @@ export interface IChatMessage {
memoryType?: string memoryType?: string
sessionId?: string sessionId?: string
createdDate: Date createdDate: Date
leadEmail?: string
} }
export interface IChatMessageFeedback { export interface IChatMessageFeedback {
@ -93,6 +94,16 @@ export interface IVariable {
createdDate: Date createdDate: Date
} }
export interface ILead {
id: string
name?: string
email?: string
phone?: string
chatflowid: string
chatId: string
createdDate: Date
}
export interface IUpsertHistory { export interface IUpsertHistory {
id: string id: string
chatflowid: string chatflowid: string
@ -200,6 +211,7 @@ export interface IncomingInput {
chatId?: string chatId?: string
stopNodeId?: string stopNodeId?: string
uploads?: IFileUpload[] uploads?: IFileUpload[]
leadEmail?: string
} }
export interface IActiveChatflows { export interface IActiveChatflows {

View File

@ -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
}

View File

@ -44,4 +44,7 @@ export class ChatMessage implements IChatMessage {
@Column({ type: 'timestamp' }) @Column({ type: 'timestamp' })
@CreateDateColumn() @CreateDateColumn()
createdDate: Date createdDate: Date
@Column({ nullable: true, type: 'text' })
leadEmail?: string
} }

View File

@ -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
}

View File

@ -5,6 +5,7 @@ import { Credential } from './Credential'
import { Tool } from './Tool' import { Tool } from './Tool'
import { Assistant } from './Assistant' import { Assistant } from './Assistant'
import { Variable } from './Variable' import { Variable } from './Variable'
import { Lead } from './Lead'
import { UpsertHistory } from './UpsertHistory' import { UpsertHistory } from './UpsertHistory'
export const entities = { export const entities = {
@ -15,5 +16,6 @@ export const entities = {
Tool, Tool,
Assistant, Assistant,
Variable, Variable,
Lead,
UpsertHistory UpsertHistory
} }

View File

@ -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`)
}
}

View File

@ -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\`;`)
}
}

View File

@ -15,6 +15,8 @@ import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntit
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213626553 } from './1707213626553-AddFeedback' import { AddFeedback1707213626553 } from './1707213626553-AddFeedback'
import { AddLead1710832127079 } from './1710832127079-AddLead'
import { AddLeadToChatMessage1711538023578 } from './1711538023578-AddLeadToChatMessage'
export const mysqlMigrations = [ export const mysqlMigrations = [
Init1693840429259, Init1693840429259,
@ -33,5 +35,7 @@ export const mysqlMigrations = [
AddVariableEntity1699325775451, AddVariableEntity1699325775451,
AddSpeechToText1706364937060, AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358, AddUpsertHistoryEntity1709814301358,
AddFeedback1707213626553 AddFeedback1707213626553,
AddLead1710832127079,
AddLeadToChatMessage1711538023578
] ]

View File

@ -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`)
}
}

View File

@ -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";`)
}
}

View File

@ -16,6 +16,8 @@ import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213601923 } from './1707213601923-AddFeedback' import { AddFeedback1707213601923 } from './1707213601923-AddFeedback'
import { FieldTypes1710497452584 } from './1710497452584-FieldTypes' import { FieldTypes1710497452584 } from './1710497452584-FieldTypes'
import { AddLead1710832137905 } from './1710832137905-AddLead'
import { AddLeadToChatMessage1711538016098 } from './1711538016098-AddLeadToChatMessage'
export const postgresMigrations = [ export const postgresMigrations = [
Init1693891895163, Init1693891895163,
@ -35,5 +37,7 @@ export const postgresMigrations = [
AddSpeechToText1706364937060, AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358, AddUpsertHistoryEntity1709814301358,
AddFeedback1707213601923, AddFeedback1707213601923,
FieldTypes1710497452584 FieldTypes1710497452584,
AddLead1710832137905,
AddLeadToChatMessage1711538016098
] ]

View File

@ -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";`)
}
}

View File

@ -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";`)
}
}

View File

@ -15,6 +15,8 @@ import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntit
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213619308 } from './1707213619308-AddFeedback' import { AddFeedback1707213619308 } from './1707213619308-AddFeedback'
import { AddLead1710832117612 } from './1710832117612-AddLead'
import { AddLeadToChatMessage1711537986113 } from './1711537986113-AddLeadToChatMessage'
export const sqliteMigrations = [ export const sqliteMigrations = [
Init1693835579790, Init1693835579790,
@ -33,5 +35,7 @@ export const sqliteMigrations = [
AddVariableEntity1699325775451, AddVariableEntity1699325775451,
AddSpeechToText1706364937060, AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358, AddUpsertHistoryEntity1709814301358,
AddFeedback1707213619308 AddFeedback1707213619308,
AddLead1710832117612,
AddLeadToChatMessage1711537986113
] ]

View File

@ -137,6 +137,7 @@ export class App {
'/api/v1/chatflows-uploads', '/api/v1/chatflows-uploads',
'/api/v1/openai-assistants-file/download', '/api/v1/openai-assistants-file/download',
'/api/v1/feedback', '/api/v1/feedback',
'/api/v1/leads',
'/api/v1/get-upload-file', '/api/v1/get-upload-file',
'/api/v1/ip' '/api/v1/ip'
] ]

View File

@ -15,6 +15,7 @@ import internalChatmessagesRouter from './internal-chat-messages'
import internalPredictionRouter from './internal-predictions' import internalPredictionRouter from './internal-predictions'
import getUploadFileRouter from './get-upload-file' import getUploadFileRouter from './get-upload-file'
import getUploadPathRouter from './get-upload-path' import getUploadPathRouter from './get-upload-path'
import leadsRouter from './leads'
import loadPromptRouter from './load-prompts' import loadPromptRouter from './load-prompts'
import marketplacesRouter from './marketplaces' import marketplacesRouter from './marketplaces'
import nodeConfigRouter from './node-configs' import nodeConfigRouter from './node-configs'
@ -55,6 +56,7 @@ router.use('/internal-chatmessage', internalChatmessagesRouter)
router.use('/internal-prediction', internalPredictionRouter) router.use('/internal-prediction', internalPredictionRouter)
router.use('/get-upload-file', getUploadFileRouter) router.use('/get-upload-file', getUploadFileRouter)
router.use('/get-upload-path', getUploadPathRouter) router.use('/get-upload-path', getUploadPathRouter)
router.use('/leads', leadsRouter)
router.use('/load-prompt', loadPromptRouter) router.use('/load-prompt', loadPromptRouter)
router.use('/marketplaces', marketplacesRouter) router.use('/marketplaces', marketplacesRouter)
router.use('/node-config', nodeConfigRouter) router.use('/node-config', nodeConfigRouter)

View File

@ -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

View File

@ -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
}

View File

@ -325,7 +325,8 @@ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInter
memoryType, memoryType,
sessionId, sessionId,
createdDate: userMessageDateTime, createdDate: userMessageDateTime,
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined,
leadEmail: incomingInput.leadEmail
} }
await utilAddChatMessage(userMessage) await utilAddChatMessage(userMessage)

View File

@ -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
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -6,7 +6,8 @@ import {
IconCopy, IconCopy,
IconMessage, IconMessage,
IconDatabaseExport, IconDatabaseExport,
IconAdjustmentsHorizontal IconAdjustmentsHorizontal,
IconUsers
} from '@tabler/icons' } from '@tabler/icons'
// constant // constant
@ -17,7 +18,8 @@ const icons = {
IconCopy, IconCopy,
IconMessage, IconMessage,
IconDatabaseExport, IconDatabaseExport,
IconAdjustmentsHorizontal IconAdjustmentsHorizontal,
IconUsers
} }
// ==============================|| SETTINGS MENU ITEMS ||============================== // // ==============================|| SETTINGS MENU ITEMS ||============================== //
@ -34,6 +36,13 @@ const settings = {
url: '', url: '',
icon: icons.IconMessage icon: icons.IconMessage
}, },
{
id: 'viewLeads',
title: 'View Leads',
type: 'item',
url: '',
icon: icons.IconUsers
},
{ {
id: 'viewUpsertHistory', id: 'viewUpsertHistory',
title: 'Upsert History', title: 'Upsert History',

View File

@ -10,10 +10,10 @@ const StatsCard = ({ title, stat }) => {
return ( return (
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}> <Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent> <CardContent>
<Typography sx={{ fontSize: 14 }} color='text.primary' gutterBottom> <Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom>
{title} {title}
</Typography> </Typography>
<Typography sx={{ fontSize: 30, fontWeight: 500 }} color='text.primary'> <Typography sx={{ fontSize: '1.5rem', fontWeight: 500 }} color='text.primary'>
{stat} {stat}
</Typography> </Typography>
</CardContent> </CardContent>

View File

@ -2,12 +2,14 @@ import PropTypes from 'prop-types'
import { useState } from 'react' import { useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material' import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material'
import { tabsClasses } from '@mui/material/Tabs'
import SpeechToText from '@/ui-component/extended/SpeechToText' import SpeechToText from '@/ui-component/extended/SpeechToText'
import RateLimit from '@/ui-component/extended/RateLimit' import RateLimit from '@/ui-component/extended/RateLimit'
import AllowedDomains from '@/ui-component/extended/AllowedDomains' import AllowedDomains from '@/ui-component/extended/AllowedDomains'
import ChatFeedback from '@/ui-component/extended/ChatFeedback' import ChatFeedback from '@/ui-component/extended/ChatFeedback'
import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import AnalyseFlow from '@/ui-component/extended/AnalyseFlow'
import StarterPrompts from '@/ui-component/extended/StarterPrompts' import StarterPrompts from '@/ui-component/extended/StarterPrompts'
import Leads from '@/ui-component/extended/Leads'
const CHATFLOW_CONFIGURATION_TABS = [ const CHATFLOW_CONFIGURATION_TABS = [
{ {
@ -33,6 +35,10 @@ const CHATFLOW_CONFIGURATION_TABS = [
{ {
label: 'Analyse Chatflow', label: 'Analyse Chatflow',
id: 'analyseChatflow' id: 'analyseChatflow'
},
{
label: 'Leads',
id: 'leads'
} }
] ]
@ -83,10 +89,19 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Tabs <Tabs
sx={{ position: 'relative', minHeight: '40px', height: '40px' }} sx={{
position: 'relative',
minHeight: '40px',
height: '40px',
[`& .${tabsClasses.scrollButtons}`]: {
'&.Mui-disabled': { opacity: 0.3 }
}
}}
value={tabValue} value={tabValue}
onChange={(event, value) => setTabValue(value)} onChange={(event, value) => setTabValue(value)}
aria-label='tabs' aria-label='tabs'
variant='scrollable'
scrollButtons='auto'
> >
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( {CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
<Tab <Tab
@ -105,6 +120,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
{item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null} {item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null}
{item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null} {item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null}
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null} {item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
</TabPanel> </TabPanel>
))} ))}
</DialogContent> </DialogContent>

View File

@ -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 (
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
{value}
</ListItemButton>
)
})
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 ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth={leads && leads.length == 0 ? 'md' : 'lg'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{dialogProps.title}
<OutlinedInput
size='small'
sx={{
ml: 3,
width: '280px',
height: '100%',
display: { xs: 'none', sm: 'flex' },
borderRadius: 2,
'& .MuiOutlinedInput-notchedOutline': {
borderRadius: 2
}
}}
variant='outlined'
placeholder='Search Name or Email or Phone'
onChange={onSearchChange}
startAdornment={
<Box
sx={{
color: theme.palette.grey[400],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mr: 1
}}
>
<IconSearch style={{ color: 'inherit', width: 16, height: 16 }} />
</Box>
}
type='search'
/>
<div style={{ flex: 1 }} />
{leads && leads.length > 0 && (
<Button variant='outlined' onClick={() => exportMessages()} startIcon={<IconFileExport />}>
Export
</Button>
)}
</div>
</DialogTitle>
<DialogContent>
{leads && leads.length == 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={leadsEmptySVG} alt='msgEmptySVG' />
</Box>
<div>No Leads</div>
</Stack>
)}
{leads && leads.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Email Address</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Created Date</TableCell>
</TableRow>
</TableHead>
<TableBody>
{leads.filter(filterLeads).map((lead, index) => (
<TableRow key={index}>
<TableCell>{lead.name}</TableCell>
<TableCell>{lead.email}</TableCell>
<TableCell>{lead.phone}</TableCell>
<TableCell>{moment(lead.createdDate).format('MMMM Do, YYYY')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ViewLeadsDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default ViewLeadsDialog

View File

@ -102,6 +102,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [chatTypeFilter, setChatTypeFilter] = useState([]) const [chatTypeFilter, setChatTypeFilter] = useState([])
const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1))
const [endDate, setEndDate] = useState(new Date()) const [endDate, setEndDate] = useState(new Date())
const [leadEmail, setLeadEmail] = useState('')
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
@ -191,6 +192,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
source: chatmsg.chatType === 'INTERNAL' ? 'UI' : 'API/Embed', source: chatmsg.chatType === 'INTERNAL' ? 'UI' : 'API/Embed',
sessionId: chatmsg.sessionId ?? null, sessionId: chatmsg.sessionId ?? null,
memoryType: chatmsg.memoryType ?? null, memoryType: chatmsg.memoryType ?? null,
email: leadEmail ?? null,
messages: [msg] messages: [msg]
} }
} else if (Object.prototype.hasOwnProperty.call(obj, chatPK)) { } else if (Object.prototype.hasOwnProperty.call(obj, chatPK)) {
@ -407,6 +409,13 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setSourceDialogOpen(true) setSourceDialogOpen(true)
} }
useEffect(() => {
const leadEmailFromChatMessages = chatMessages.filter((message) => message.type === 'userMessage' && message.leadEmail)
if (leadEmailFromChatMessages.length) {
setLeadEmail(leadEmailFromChatMessages[0].leadEmail)
}
}, [chatMessages, selectedMessageIndex])
useEffect(() => { useEffect(() => {
if (getChatmessageFromPKApi.data) { if (getChatmessageFromPKApi.data) {
getChatMessages(getChatmessageFromPKApi.data) getChatMessages(getChatmessageFromPKApi.data)
@ -450,6 +459,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setStartDate(new Date().setMonth(new Date().getMonth() - 1)) setStartDate(new Date().setMonth(new Date().getMonth() - 1))
setEndDate(new Date()) setEndDate(new Date())
setStats([]) setStats([])
setLeadEmail('')
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -639,6 +649,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
Memory:&nbsp;<b>{chatMessages[1].memoryType}</b> Memory:&nbsp;<b>{chatMessages[1].memoryType}</b>
</div> </div>
)} )}
{leadEmail && (
<div>
Email:&nbsp;<b>{leadEmail}</b>
</div>
)}
</div> </div>
<div <div
style={{ style={{
@ -675,6 +690,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
)} )}
<div <div
style={{ style={{
display: 'flex',
flexDirection: 'column',
marginLeft: '20px', marginLeft: '20px',
border: '1px solid #e0e0e0', border: '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px` borderRadius: `${customization.borderRadius}px`

View File

@ -0,0 +1,179 @@
import { useDispatch } from 'react-redux'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
// material-ui
import { Button, Box, OutlinedInput, Typography } from '@mui/material'
import { IconX } from '@tabler/icons'
// Project import
import { StyledButton } from '@/ui-component/button/StyledButton'
import { SwitchInput } from '@/ui-component/switch/Switch'
// store
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions'
import useNotifier from '@/utils/useNotifier'
// API
import chatflowsApi from '@/api/chatflows'
const formTitle = `Hey 👋 thanks for your interest!
Let us know where we can reach you`
const endTitle = `Thank you!
What can I do for you?`
const Leads = ({ dialogProps }) => {
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
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 (
<>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'start',
gap: 3,
mb: 2
}}
>
<SwitchInput label='Enable Lead Capture' onChange={(value) => handleChange('status', value)} value={leadsConfig.status} />
{leadsConfig && leadsConfig['status'] && (
<>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 1 }}>
<Typography>Form Title</Typography>
<OutlinedInput
id='form-title'
type='text'
fullWidth
multiline={true}
minRows={4}
value={leadsConfig.title}
placeholder={formTitle}
name='form-title'
size='small'
onChange={(e) => {
handleChange('title', e.target.value)
}}
/>
</Box>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 1 }}>
<Typography>Message after lead captured</Typography>
<OutlinedInput
id='success-message'
type='text'
fullWidth
multiline={true}
minRows={4}
value={leadsConfig.successMessage}
placeholder={endTitle}
name='form-title'
size='small'
onChange={(e) => {
handleChange('successMessage', e.target.value)
}}
/>
</Box>
<Typography variant='h4'>Form fields</Typography>
<Box sx={{ width: '100%' }}>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 1 }}>
<SwitchInput label='Name' onChange={(value) => handleChange('name', value)} value={leadsConfig.name} />
<SwitchInput
label='Email Address'
onChange={(value) => handleChange('email', value)}
value={leadsConfig.email}
/>
<SwitchInput label='Phone' onChange={(value) => handleChange('phone', value)} value={leadsConfig.phone} />
</Box>
</Box>
</>
)}
</Box>
<StyledButton
disabled={!leadsConfig['name'] && !leadsConfig['phone'] && !leadsConfig['email'] && leadsConfig['status']}
style={{ marginBottom: 10, marginTop: 10 }}
variant='contained'
onClick={onSave}
>
Save
</StyledButton>
</>
)
}
Leads.propTypes = {
dialogProps: PropTypes.object
}
export default Leads

View File

@ -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 chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`)
const obj = {} const obj = { ...saveObj }
if (chatId) obj.chatId = chatId if (chatId) obj.chatId = chatId
if (!chatDetails) { 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) => { export const unshiftFiles = (configData) => {
const filesConfig = configData.find((config) => config.name === 'files') const filesConfig = configData.find((config) => config.name === 'files')
if (filesConfig) { if (filesConfig) {

View File

@ -28,6 +28,7 @@ import useApi from '@/hooks/useApi'
import { generateExportFlowData } from '@/utils/genericHelper' import { generateExportFlowData } from '@/utils/genericHelper'
import { uiBaseURL } from '@/store/constant' import { uiBaseURL } from '@/store/constant'
import { SET_CHATFLOW } from '@/store/actions' import { SET_CHATFLOW } from '@/store/actions'
import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog'
// ==============================|| CANVAS HEADER ||============================== // // ==============================|| CANVAS HEADER ||============================== //
@ -46,6 +47,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [apiDialogProps, setAPIDialogProps] = useState({}) const [apiDialogProps, setAPIDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
const [viewLeadsDialogOpen, setViewLeadsDialogOpen] = useState(false)
const [viewLeadsDialogProps, setViewLeadsDialogProps] = useState({})
const [upsertHistoryDialogOpen, setUpsertHistoryDialogOpen] = useState(false) const [upsertHistoryDialogOpen, setUpsertHistoryDialogOpen] = useState(false)
const [upsertHistoryDialogProps, setUpsertHistoryDialogProps] = useState({}) const [upsertHistoryDialogProps, setUpsertHistoryDialogProps] = useState({})
const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false) const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false)
@ -65,6 +68,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
chatflow: chatflow chatflow: chatflow
}) })
setViewMessagesDialogOpen(true) setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') {
setViewLeadsDialogProps({
title: 'View Leads',
chatflow: chatflow
})
setViewLeadsDialogOpen(true)
} else if (setting === 'viewUpsertHistory') { } else if (setting === 'viewUpsertHistory') {
setUpsertHistoryDialogProps({ setUpsertHistoryDialogProps({
title: 'View Upsert History', title: 'View Upsert History',
@ -402,6 +411,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
dialogProps={viewMessagesDialogProps} dialogProps={viewMessagesDialogProps}
onCancel={() => setViewMessagesDialogOpen(false)} onCancel={() => setViewMessagesDialogOpen(false)}
/> />
<ViewLeadsDialog show={viewLeadsDialogOpen} dialogProps={viewLeadsDialogProps} onCancel={() => setViewLeadsDialogOpen(false)} />
<UpsertHistoryDialog <UpsertHistoryDialog
show={upsertHistoryDialogOpen} show={upsertHistoryDialogOpen}
dialogProps={upsertHistoryDialogProps} dialogProps={upsertHistoryDialogProps}

View File

@ -47,6 +47,7 @@ import chatmessageApi from '@/api/chatmessage'
import chatflowsApi from '@/api/chatflows' import chatflowsApi from '@/api/chatflows'
import predictionApi from '@/api/prediction' import predictionApi from '@/api/prediction'
import chatmessagefeedbackApi from '@/api/chatmessagefeedback' import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
import leadsApi from '@/api/lead'
// Hooks // Hooks
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
@ -55,7 +56,7 @@ import useApi from '@/hooks/useApi'
import { baseURL, maxScroll } from '@/store/constant' import { baseURL, maxScroll } from '@/store/constant'
// Utils // Utils
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from '@/utils/genericHelper' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'
const messageImageStyle = { const messageImageStyle = {
width: '128px', width: '128px',
@ -91,10 +92,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([]) const [starterPrompts, setStarterPrompts] = useState([])
// feedback
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
const [feedbackId, setFeedbackId] = useState('') const [feedbackId, setFeedbackId] = useState('')
const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false) const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false)
// leads
const [leadsConfig, setLeadsConfig] = useState(null)
const [leadName, setLeadName] = useState('')
const [leadEmail, setLeadEmail] = useState('')
const [leadPhone, setLeadPhone] = useState('')
const [isLeadSaving, setIsLeadSaving] = useState(false)
const [isLeadSaved, setIsLeadSaved] = useState(false)
// drag & drop and file input // drag & drop and file input
const fileUploadRef = useRef(null) const fileUploadRef = useRef(null)
const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false)
@ -414,6 +425,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
chatId chatId
} }
if (urls && urls.length > 0) params.uploads = urls if (urls && urls.length > 0) params.uploads = urls
if (leadEmail) params.leadEmail = leadEmail
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
@ -573,6 +585,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
if (config.chatFeedback) { if (config.chatFeedback) {
setChatFeedbackStatus(config.chatFeedback.status) 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 // eslint-disable-next-line react-hooks/exhaustive-deps
@ -605,6 +631,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
setIsRecording(false) setIsRecording(false)
// leads
const savedLead = getLocalStorageChatflow(chatflowid)?.lead
if (savedLead) {
setIsLeadSaved(!!savedLead)
setLeadEmail(savedLead.email)
}
// SocketIO // SocketIO
socket = socketIOClient(baseURL) 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 ( return (
<div onDragEnter={handleDrag}> <div onDragEnter={handleDrag}>
{isDragActive && ( {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 // The latest message sent by the user will be animated while waiting for a response
<Box <Box
sx={{ sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : '' background:
message.type === 'apiMessage' || message.type === 'leadCaptureMessage'
? theme.palette.asyncSelect.main
: ''
}} }}
key={index} key={index}
style={{ display: 'flex' }} style={{ display: 'flex' }}
@ -778,14 +844,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
} }
> >
{/* Display the correct icon depending on the message type */} {/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? ( {message.type === 'apiMessage' || message.type === 'leadCaptureMessage' ? (
<img src={robotPNG} alt='AI' width='30' height='30' className='boticon' /> <img src={robotPNG} alt='AI' width='30' height='30' className='boticon' />
) : ( ) : (
<img src={userPNG} alt='Me' width='30' height='30' className='usericon' /> <img src={userPNG} alt='Me' width='30' height='30' className='usericon' />
)} )}
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}> <div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%'
}}
>
{message.usedTools && ( {message.usedTools && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}> <div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.usedTools.map((tool, index) => { {message.usedTools.map((tool, index) => {
return ( return (
<Chip <Chip
@ -816,7 +894,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
{message.fileUploads.map((item, index) => { {message.fileUploads.map((item, index) => {
return ( return (
<> <>
{item.mime.startsWith('image/') ? ( {item?.mime?.startsWith('image/') ? (
<Card <Card
key={index} key={index}
sx={{ sx={{
@ -848,36 +926,122 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</div> </div>
)} )}
<div className='markdownanswer'> <div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */} {message.type === 'leadCaptureMessage' &&
<MemoizedReactMarkdown !getLocalStorageChatflow(chatflowid)?.lead &&
remarkPlugins={[remarkGfm, remarkMath]} leadsConfig.status ? (
rehypePlugins={[rehypeMathjax, rehypeRaw]} <Box
components={{ sx={{
code({ inline, className, children, ...props }) { display: 'flex',
const match = /language-(\w+)/.exec(className || '') flexDirection: 'column',
return !inline ? ( gap: 2,
<CodeBlock marginTop: 2
key={Math.random()} }}
chatflowid={chatflowid} >
isDialog={isDialog} <Typography sx={{ lineHeight: '1.5rem', whiteSpace: 'pre-line' }}>
language={(match && match[1]) || ''} {leadsConfig.title || 'Let us know where we can reach you:'}
value={String(children).replace(/\n$/, '')} </Typography>
{...props} <form
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
width: isDialog ? '50%' : '100%'
}}
onSubmit={handleLeadCaptureSubmit}
>
{leadsConfig.name && (
<OutlinedInput
id='leadName'
type='text'
fullWidth
placeholder='Name'
name='leadName'
value={leadName}
// eslint-disable-next-line
autoFocus={true}
onChange={(e) => setLeadName(e.target.value)}
/> />
) : ( )}
<code className={className} {...props}> {leadsConfig.email && (
{children} <OutlinedInput
</code> id='leadEmail'
) type='email'
} fullWidth
}} placeholder='Email Address'
> name='leadEmail'
{message.message} value={leadEmail}
</MemoizedReactMarkdown> onChange={(e) => setLeadEmail(e.target.value)}
/>
)}
{leadsConfig.phone && (
<OutlinedInput
id='leadPhone'
type='number'
fullWidth
placeholder='Phone Number'
name='leadPhone'
value={leadPhone}
onChange={(e) => setLeadPhone(e.target.value)}
/>
)}
<Box
sx={{
display: 'flex',
alignItems: 'center'
}}
>
<Button
variant='outlined'
fullWidth
type='submit'
sx={{ borderRadius: '20px' }}
>
{isLeadSaving ? 'Saving...' : 'Save'}
</Button>
</Box>
</form>
</Box>
) : (
<>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
</>
)}
</div> </div>
{message.type === 'apiMessage' && message.id && chatFeedbackStatus ? ( {message.type === 'apiMessage' && message.id && chatFeedbackStatus ? (
<> <>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 1 }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
<CopyToClipboardButton onClick={() => copyMessageToClipboard(message.message)} /> <CopyToClipboardButton onClick={() => copyMessageToClipboard(message.message)} />
{!message.feedback || {!message.feedback ||
message.feedback.rating === '' || message.feedback.rating === '' ||
@ -901,11 +1065,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</> </>
) : null} ) : null}
{message.fileAnnotations && ( {message.fileAnnotations && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}> <div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.fileAnnotations.map((fileAnnotation, index) => { {message.fileAnnotations.map((fileAnnotation, index) => {
return ( return (
<Button <Button
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }} sx={{
fontSize: '0.85rem',
textTransform: 'none',
mb: 1
}}
key={index} key={index}
variant='outlined' variant='outlined'
onClick={() => downloadFile(fileAnnotation)} onClick={() => downloadFile(fileAnnotation)}
@ -918,7 +1092,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</div> </div>
)} )}
{message.sourceDocuments && ( {message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}> <div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{removeDuplicateURL(message).map((source, index) => { {removeDuplicateURL(message).map((source, index) => {
const URL = const URL =
source.metadata && source.metadata.source source.metadata && source.metadata.source
@ -1076,7 +1256,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
// eslint-disable-next-line // eslint-disable-next-line
autoFocus autoFocus
sx={{ width: '100%' }} sx={{ width: '100%' }}
disabled={loading || !chatflowid} disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
onKeyDown={handleEnter} onKeyDown={handleEnter}
id='userInput' id='userInput'
name='userInput' name='userInput'
@ -1091,11 +1271,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
<IconButton <IconButton
onClick={handleUploadClick} onClick={handleUploadClick}
type='button' type='button'
disabled={loading || !chatflowid} disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='start' edge='start'
> >
<IconPhotoPlus <IconPhotoPlus
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'} color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/> />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
@ -1108,20 +1294,28 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
<IconButton <IconButton
onClick={() => onMicrophonePressed()} onClick={() => onMicrophonePressed()}
type='button' type='button'
disabled={loading || !chatflowid} disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end' edge='end'
> >
<IconMicrophone <IconMicrophone
className={'start-recording-button'} className={'start-recording-button'}
color={ color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5' loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
} }
/> />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
)} )}
<InputAdornment position='end' sx={{ padding: '15px' }}> <InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'> <IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
{loading ? ( {loading ? (
<div> <div>
<CircularProgress color='inherit' size={20} /> <CircularProgress color='inherit' size={20} />
@ -1130,7 +1324,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
// Send icon SVG in input field // Send icon SVG in input field
<IconSend <IconSend
color={ color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5' loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
} }
/> />
)} )}

View File

@ -23,6 +23,9 @@ import useNotifier from '@/utils/useNotifier'
// Const // Const
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Utils
import { getLocalStorageChatflow, removeLocalStorageChatHistory } from '@/utils/genericHelper'
export const ChatPopUp = ({ chatflowid }) => { export const ChatPopUp = ({ chatflowid }) => {
const theme = useTheme() const theme = useTheme()
const { confirm } = useConfirm() const { confirm } = useConfirm()
@ -86,11 +89,10 @@ export const ChatPopUp = ({ chatflowid }) => {
if (isConfirmed) { if (isConfirmed) {
try { try {
const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) const objChatDetails = getLocalStorageChatflow(chatflowid)
if (!chatDetails) return if (!objChatDetails.chatId) return
const objChatDetails = JSON.parse(chatDetails)
await chatmessageApi.deleteChatmessage(chatflowid, { chatId: objChatDetails.chatId, chatType: 'INTERNAL' }) await chatmessageApi.deleteChatmessage(chatflowid, { chatId: objChatDetails.chatId, chatType: 'INTERNAL' })
localStorage.removeItem(`${chatflowid}_INTERNAL`) removeLocalStorageChatHistory(chatflowid)
resetChatDialog() resetChatDialog()
enqueueSnackbar({ enqueueSnackbar({
message: 'Succesfully cleared all chat history', message: 'Succesfully cleared all chat history',