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
31 changed files with 979 additions and 57 deletions
+9
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

+11 -2
View File
@@ -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',
@@ -10,10 +10,10 @@ const StatsCard = ({ title, stat }) => {
return (
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Typography sx={{ fontSize: 14 }} color='text.primary' gutterBottom>
<Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom>
{title}
</Typography>
<Typography sx={{ fontSize: 30, fontWeight: 500 }} color='text.primary'>
<Typography sx={{ fontSize: '1.5rem', fontWeight: 500 }} color='text.primary'>
{stat}
</Typography>
</CardContent>
@@ -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 }) => {
</DialogTitle>
<DialogContent>
<Tabs
sx={{ position: 'relative', minHeight: '40px', height: '40px' }}
sx={{
position: 'relative',
minHeight: '40px',
height: '40px',
[`& .${tabsClasses.scrollButtons}`]: {
'&.Mui-disabled': { opacity: 0.3 }
}
}}
value={tabValue}
onChange={(event, value) => setTabValue(value)}
aria-label='tabs'
variant='scrollable'
scrollButtons='auto'
>
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
<Tab
@@ -105,6 +120,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
{item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null}
{item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null}
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
</TabPanel>
))}
</DialogContent>
@@ -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
@@ -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:&nbsp;<b>{chatMessages[1].memoryType}</b>
</div>
)}
{leadEmail && (
<div>
Email:&nbsp;<b>{leadEmail}</b>
</div>
)}
</div>
<div
style={{
@@ -675,6 +690,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
)}
<div
style={{
display: 'flex',
flexDirection: 'column',
marginLeft: '20px',
border: '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px`
@@ -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
+30 -2
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 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) {
@@ -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)}
/>
<ViewLeadsDialog show={viewLeadsDialogOpen} dialogProps={viewLeadsDialogProps} onCancel={() => setViewLeadsDialogOpen(false)} />
<UpsertHistoryDialog
show={upsertHistoryDialogOpen}
dialogProps={upsertHistoryDialogProps}
+240 -42
View File
@@ -47,6 +47,7 @@ import chatmessageApi from '@/api/chatmessage'
import chatflowsApi from '@/api/chatflows'
import predictionApi from '@/api/prediction'
import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
import leadsApi from '@/api/lead'
// Hooks
import useApi from '@/hooks/useApi'
@@ -55,7 +56,7 @@ import useApi from '@/hooks/useApi'
import { baseURL, maxScroll } from '@/store/constant'
// Utils
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from '@/utils/genericHelper'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'
const messageImageStyle = {
width: '128px',
@@ -91,10 +92,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([])
// feedback
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
const [feedbackId, setFeedbackId] = useState('')
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
const fileUploadRef = useRef(null)
const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false)
@@ -414,6 +425,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
chatId
}
if (urls && urls.length > 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 (
<div onDragEnter={handleDrag}>
{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
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
background:
message.type === 'apiMessage' || message.type === 'leadCaptureMessage'
? theme.palette.asyncSelect.main
: ''
}}
key={index}
style={{ display: 'flex' }}
@@ -778,14 +844,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
}
>
{/* 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={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 && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.usedTools.map((tool, index) => {
return (
<Chip
@@ -816,7 +894,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
{message.fileUploads.map((item, index) => {
return (
<>
{item.mime.startsWith('image/') ? (
{item?.mime?.startsWith('image/') ? (
<Card
key={index}
sx={{
@@ -848,36 +926,122 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</div>
)}
<div className='markdownanswer'>
{/* 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}
{message.type === 'leadCaptureMessage' &&
!getLocalStorageChatflow(chatflowid)?.lead &&
leadsConfig.status ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
marginTop: 2
}}
>
<Typography sx={{ lineHeight: '1.5rem', whiteSpace: 'pre-line' }}>
{leadsConfig.title || 'Let us know where we can reach you:'}
</Typography>
<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}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
)}
{leadsConfig.email && (
<OutlinedInput
id='leadEmail'
type='email'
fullWidth
placeholder='Email Address'
name='leadEmail'
value={leadEmail}
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>
{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)} />
{!message.feedback ||
message.feedback.rating === '' ||
@@ -901,11 +1065,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</>
) : null}
{message.fileAnnotations && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{message.fileAnnotations.map((fileAnnotation, index) => {
return (
<Button
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
sx={{
fontSize: '0.85rem',
textTransform: 'none',
mb: 1
}}
key={index}
variant='outlined'
onClick={() => downloadFile(fileAnnotation)}
@@ -918,7 +1092,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</div>
)}
{message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{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
<IconButton
onClick={handleUploadClick}
type='button'
disabled={loading || !chatflowid}
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='start'
>
<IconPhotoPlus
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
</IconButton>
</InputAdornment>
@@ -1108,20 +1294,28 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
<IconButton
onClick={() => onMicrophonePressed()}
type='button'
disabled={loading || !chatflowid}
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
<IconMicrophone
className={'start-recording-button'}
color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
</IconButton>
</InputAdornment>
)}
<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 ? (
<div>
<CircularProgress color='inherit' size={20} />
@@ -1130,7 +1324,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
)}
@@ -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',