mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 13:00:56 +03:00
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:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user