Feat/FlowConfigDialog UI Redesign (#6229)

* enhance ChatflowConfigurationDialog with new configuration sections and improved UI

* fix: remove background color override for MuiSvgIcon in dark mode
This commit is contained in:
Henry Heng
2026-04-16 14:57:43 +01:00
committed by GitHub
parent 73d57db9d2
commit 85df44bca4
16 changed files with 827 additions and 351 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

+1 -2
View File
@@ -45,8 +45,7 @@ export default function componentStyleOverrides(theme) {
MuiSvgIcon: {
styleOverrides: {
root: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit',
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryLight : 'inherit'
color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'
}
}
},
@@ -1,11 +1,33 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material'
import { tabsClasses } from '@mui/material/Tabs'
import { useSelector } from 'react-redux'
import { Box, Dialog, DialogContent, DialogTitle, Typography, IconButton } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import {
IconX,
IconShieldLock,
IconWorldWww,
IconUserPlus,
IconMessageChatbot,
IconArrowForwardUp,
IconThumbUp,
IconMicrophone,
IconVolume,
IconUpload,
IconChartBar,
IconCode,
IconServer,
IconAdjustments
} from '@tabler/icons-react'
import PerfectScrollbar from 'react-perfect-scrollbar'
// Section components
import SpeechToText from '@/ui-component/extended/SpeechToText'
import TextToSpeech from '@/ui-component/extended/TextToSpeech'
import Security from '@/ui-component/extended/Security'
import RateLimit from '@/ui-component/extended/RateLimit'
import AllowedDomains from '@/ui-component/extended/AllowedDomains'
import OverrideConfig from '@/ui-component/extended/OverrideConfig'
import ChatFeedback from '@/ui-component/extended/ChatFeedback'
import AnalyseFlow from '@/ui-component/extended/AnalyseFlow'
import StarterPrompts from '@/ui-component/extended/StarterPrompts'
@@ -15,146 +37,448 @@ import FileUpload from '@/ui-component/extended/FileUpload'
import PostProcessing from '@/ui-component/extended/PostProcessing'
import McpServer from '@/ui-component/extended/McpServer'
const CHATFLOW_CONFIGURATION_TABS = [
const CONFIGURATION_GROUPS = [
{
label: 'Security',
id: 'security'
label: 'General',
sections: [
{
label: 'Rate Limit',
id: 'rateLimit',
icon: IconShieldLock,
description: 'Limit API requests per time window'
},
{
label: 'Allowed Domains',
id: 'allowedDomains',
icon: IconWorldWww,
description: 'Restrict chatbot to specific domains'
},
{
label: 'Leads',
id: 'leads',
icon: IconUserPlus,
description: 'Capture visitor contact information'
}
]
},
{
label: 'Starter Prompts',
id: 'conversationStarters'
label: 'Chat',
sections: [
{
label: 'Starter Prompts',
id: 'conversationStarters',
icon: IconMessageChatbot,
description: 'Suggested prompts for new conversations'
},
{
label: 'Follow-up Prompts',
id: 'followUpPrompts',
icon: IconArrowForwardUp,
description: 'Auto-generate follow-up questions'
},
{
label: 'Chat Feedback',
id: 'chatFeedback',
icon: IconThumbUp,
description: 'Allow users to rate responses'
}
]
},
{
label: 'Follow-up Prompts',
id: 'followUpPrompts'
label: 'Media & Files',
sections: [
{
label: 'Speech to Text',
id: 'speechToText',
icon: IconMicrophone,
description: 'Voice input transcription'
},
{
label: 'Text to Speech',
id: 'textToSpeech',
icon: IconVolume,
description: 'Audio response playback'
},
{
label: 'File Upload',
id: 'fileUpload',
icon: IconUpload,
description: 'Allow file uploads in chat'
}
]
},
{
label: 'Speech to Text',
id: 'speechToText'
},
{
label: 'Text to Speech',
id: 'textToSpeech'
},
{
label: 'Chat Feedback',
id: 'chatFeedback'
},
{
label: 'Analyse Chatflow',
id: 'analyseChatflow'
},
{
label: 'Leads',
id: 'leads'
},
{
label: 'File Upload',
id: 'fileUpload'
},
{
label: 'Post Processing',
id: 'postProcessing'
},
{
label: 'MCP Server',
id: 'mcpServer'
label: 'Advanced',
sections: [
{
label: 'Analytics',
id: 'analyseChatflow',
icon: IconChartBar,
description: 'Connect analytics providers'
},
{
label: 'Post Processing',
id: 'postProcessing',
icon: IconCode,
description: 'Custom JavaScript post-processing'
},
{
label: 'MCP Server',
id: 'mcpServer',
icon: IconServer,
description: 'Model Context Protocol server'
},
{
label: 'Override Config',
id: 'overrideConfig',
icon: IconAdjustments,
description: 'Override flow configuration via API'
}
]
}
]
function TabPanel(props) {
const { children, value, index, ...other } = props
return (
<div
role='tabpanel'
hidden={value !== index}
id={`chatflow-config-tabpanel-${index}`}
aria-labelledby={`chatflow-config-tab-${index}`}
style={{ width: '100%', paddingTop: '1rem' }}
{...other}
>
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
</div>
)
}
function getSectionStatus(sectionId, chatflow) {
if (!chatflow) return false
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
}
let chatbotConfig = {}
let apiConfig = {}
try {
chatbotConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {}
apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {}
} catch {
return false
}
function a11yProps(index) {
return {
id: `chatflow-config-tab-${index}`,
'aria-controls': `chatflow-config-tabpanel-${index}`
switch (sectionId) {
case 'rateLimit':
return apiConfig?.rateLimit?.status === true
case 'allowedDomains':
return Array.isArray(chatbotConfig?.allowedOrigins) && chatbotConfig.allowedOrigins.some((o) => o && o.trim() !== '')
case 'leads':
return chatbotConfig?.leads?.status === true
case 'conversationStarters': {
const sp = chatbotConfig?.starterPrompts
if (!sp) return false
return Object.values(sp).some((entry) => entry?.prompt && entry.prompt.trim() !== '')
}
case 'followUpPrompts':
return chatbotConfig?.followUpPrompts?.status === true
case 'chatFeedback':
return chatbotConfig?.chatFeedback?.status === true
case 'speechToText': {
const stt = chatbotConfig?.speechToText
if (!stt) return false
return Object.values(stt).some((provider) => provider?.status === true)
}
case 'textToSpeech': {
const tts = chatbotConfig?.textToSpeech
if (!tts) return false
return Object.values(tts).some((provider) => provider?.status === true)
}
case 'fileUpload':
return chatbotConfig?.fullFileUpload?.status === true
case 'analyseChatflow': {
const ap = chatbotConfig?.analyticsProviders
if (!ap) return false
return Object.values(ap).some((provider) => provider?.status === true)
}
case 'postProcessing':
return chatbotConfig?.postProcessing?.status === true
case 'mcpServer':
return chatbotConfig?.mcpServer?.status === true
case 'overrideConfig':
return apiConfig?.overrideConfig?.status === true
default:
return false
}
}
// Flatten all sections for quick lookup
const ALL_SECTIONS = CONFIGURATION_GROUPS.flatMap((g) => g.sections)
const SIDEBAR_WIDTH = 220
const ChatflowConfigurationDialog = ({ show, isAgentCanvas, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const [tabValue, setTabValue] = useState(0)
const theme = useTheme()
const chatflow = useSelector((state) => state.canvas.chatflow)
const customization = useSelector((state) => state.customization)
const filteredTabs = CHATFLOW_CONFIGURATION_TABS.filter((tab) => !isAgentCanvas || !tab.hideInAgentFlow)
const [activeSection, setActiveSection] = useState('rateLimit')
const [mcpServerEnabled, setMcpServerEnabled] = useState(false)
const isDark = theme.palette.mode === 'dark' || customization?.isDarkMode
// Filter groups/sections based on agent canvas
const filteredGroups = useMemo(() => {
return CONFIGURATION_GROUPS.map((group) => ({
...group,
sections: group.sections.filter((section) => !isAgentCanvas || !section.hideInAgentFlow)
})).filter((group) => group.sections.length > 0)
}, [isAgentCanvas])
// Get all section IDs for validation
const allSectionIds = useMemo(() => {
return filteredGroups.flatMap((g) => g.sections.map((s) => s.id))
}, [filteredGroups])
// Reset activeSection if current one is filtered out
const currentSection = allSectionIds.includes(activeSection) ? activeSection : allSectionIds[0] || 'rateLimit'
const currentSectionData = ALL_SECTIONS.find((s) => s.id === currentSection)
const renderContent = () => {
const props = { dialogProps }
switch (currentSection) {
case 'rateLimit':
return <RateLimit {...props} hideTitle />
case 'allowedDomains':
return <AllowedDomains {...props} hideTitle />
case 'leads':
return <Leads {...props} />
case 'conversationStarters':
return <StarterPrompts {...props} />
case 'followUpPrompts':
return <FollowUpPrompts {...props} />
case 'chatFeedback':
return <ChatFeedback {...props} />
case 'speechToText':
return <SpeechToText {...props} />
case 'textToSpeech':
return <TextToSpeech {...props} />
case 'fileUpload':
return <FileUpload {...props} />
case 'analyseChatflow':
return <AnalyseFlow {...props} />
case 'postProcessing':
return <PostProcessing {...props} />
case 'mcpServer':
return <McpServer {...props} onStatusChange={(enabled) => setMcpServerEnabled(enabled)} />
case 'overrideConfig':
return <OverrideConfig {...props} hideTitle />
default:
return null
}
}
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth={'lg'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
maxWidth={false}
PaperProps={{
sx: {
width: 960,
maxWidth: '95vw',
height: '82vh',
maxHeight: '82vh',
display: 'flex',
flexDirection: 'column',
borderRadius: '12px',
overflow: 'hidden'
}
}}
aria-labelledby='chatflow-config-dialog-title'
>
<DialogTitle sx={{ fontSize: '1.25rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Tabs
{/* Header */}
<DialogTitle
id='chatflow-config-dialog-title'
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
py: 2,
px: 3,
borderBottom: '1px solid',
borderColor: 'divider',
minHeight: 'auto',
bgcolor: isDark ? 'background.paper' : '#fff'
}}
>
<Typography sx={{ fontSize: '1.125rem', fontWeight: 600, color: 'text.primary' }}>{dialogProps.title}</Typography>
<IconButton
size='small'
onClick={onCancel}
sx={{
position: 'relative',
minHeight: '40px',
height: '40px',
[`& .${tabsClasses.scrollButtons}`]: {
'&.Mui-disabled': { opacity: 0.3 }
}
color: 'text.secondary',
'&:hover': { bgcolor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)' }
}}
value={tabValue}
onChange={(event, value) => setTabValue(value)}
aria-label='tabs'
variant='scrollable'
scrollButtons='auto'
>
{filteredTabs.map((item, index) => (
<Tab
sx={{
minHeight: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
mb: 1
}}
key={item.id}
label={item.label}
{...a11yProps(index)}
></Tab>
))}
</Tabs>
{filteredTabs.map((item, index) => (
<TabPanel key={item.id} value={tabValue} index={index}>
{item.id === 'security' && <Security dialogProps={dialogProps} />}
{item.id === 'conversationStarters' ? <StarterPrompts dialogProps={dialogProps} /> : null}
{item.id === 'followUpPrompts' ? <FollowUpPrompts dialogProps={dialogProps} /> : null}
{item.id === 'speechToText' ? <SpeechToText dialogProps={dialogProps} /> : null}
{item.id === 'textToSpeech' ? <TextToSpeech dialogProps={dialogProps} /> : null}
{item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null}
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
{item.id === 'fileUpload' ? <FileUpload dialogProps={dialogProps} /> : null}
{item.id === 'postProcessing' ? <PostProcessing dialogProps={dialogProps} /> : null}
{item.id === 'mcpServer' ? <McpServer dialogProps={dialogProps} /> : null}
</TabPanel>
))}
<IconX size={18} />
</IconButton>
</DialogTitle>
{/* Body: Sidebar + Content */}
<DialogContent sx={{ display: 'flex', p: 0, overflow: 'hidden', flex: 1 }}>
{/* Sidebar */}
<Box
sx={{
width: SIDEBAR_WIDTH,
minWidth: SIDEBAR_WIDTH,
borderRight: '1px solid',
borderColor: 'divider',
bgcolor: isDark ? 'rgba(0,0,0,0.15)' : theme.palette.grey[50],
display: 'flex',
flexDirection: 'column'
}}
>
<PerfectScrollbar style={{ height: '100%', overflowX: 'hidden' }}>
<Box sx={{ py: 2, px: 1 }}>
{filteredGroups.map((group, groupIndex) => (
<Box key={group.label} sx={{ mb: 0.5 }}>
{/* Group label */}
<Typography
sx={{
fontSize: '0.6875rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
color: isDark ? 'grey.500' : 'grey.500',
px: 1.5,
pt: groupIndex > 0 ? 2 : 0.5,
pb: 0.75
}}
>
{group.label}
</Typography>
{/* Section items */}
{group.sections.map((section) => {
const isActive = currentSection === section.id
const isEnabled =
section.id === 'mcpServer' ? mcpServerEnabled : getSectionStatus(section.id, chatflow)
const SectionIcon = section.icon
return (
<Box
key={section.id}
onClick={() => setActiveSection(section.id)}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
px: 1.5,
py: 0.875,
borderRadius: '8px',
cursor: 'pointer',
position: 'relative',
color: isActive ? 'primary.main' : isDark ? 'grey.300' : 'grey.700',
bgcolor: isActive
? isDark
? 'rgba(33, 150, 243, 0.12)'
: 'rgba(33, 150, 243, 0.06)'
: 'transparent',
transition: 'all 0.15s ease',
'&:hover': {
bgcolor: isActive
? isDark
? 'rgba(33, 150, 243, 0.16)'
: 'rgba(33, 150, 243, 0.08)'
: isDark
? 'rgba(255,255,255,0.04)'
: 'rgba(0,0,0,0.03)'
},
userSelect: 'none'
}}
>
{/* Icon */}
<SectionIcon
size={17}
stroke={1.5}
style={{
flexShrink: 0,
opacity: isActive ? 1 : 0.7
}}
/>
{/* Label */}
<Typography
sx={{
fontSize: '0.8125rem',
fontWeight: isActive ? 600 : 400,
color: 'inherit',
lineHeight: 1.3,
flex: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{section.label}
</Typography>
{/* Status badge - only show when enabled */}
{isEnabled && (
<Box
sx={{
px: 0.875,
py: 0.125,
borderRadius: '4px',
fontSize: '0.625rem',
fontWeight: 600,
lineHeight: 1.6,
letterSpacing: '0.02em',
flexShrink: 0,
bgcolor: isDark ? 'rgba(34,197,94,0.15)' : 'rgba(34,197,94,0.1)',
color: isDark ? '#4ade80' : '#16a34a'
}}
>
ON
</Box>
)}
</Box>
)
})}
</Box>
))}
</Box>
</PerfectScrollbar>
</Box>
{/* Content area */}
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
bgcolor: isDark ? 'background.paper' : '#fff'
}}
>
<PerfectScrollbar style={{ height: '100%' }}>
<Box sx={{ px: 3.5, py: 3 }}>
{/* Section header */}
<Box sx={{ mb: 2.5 }}>
<Typography
sx={{
fontSize: '1rem',
fontWeight: 600,
color: 'text.primary',
mb: 0.25
}}
>
{currentSectionData?.label || ''}
</Typography>
{currentSectionData?.description && (
<Typography
sx={{
fontSize: '0.8rem',
color: 'text.secondary',
lineHeight: 1.5,
opacity: isDark ? 0.8 : 1
}}
>
{currentSectionData.description}
</Typography>
)}
</Box>
{/* Section content */}
{renderContent()}
</Box>
</PerfectScrollbar>
</Box>
</DialogContent>
</Dialog>
) : null
@@ -17,7 +17,7 @@ import useNotifier from '@/utils/useNotifier'
// API
import chatflowsApi from '@/api/chatflows'
const AllowedDomains = ({ dialogProps, onConfirm }) => {
const AllowedDomains = ({ dialogProps, onConfirm, hideTitle = false }) => {
const dispatch = useDispatch()
useNotifier()
@@ -119,14 +119,16 @@ const AllowedDomains = ({ dialogProps, onConfirm }) => {
}, [dialogProps])
return (
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Allowed Domains
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={'Your chatbot will only work when used from the following domains.'}
/>
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{!hideTitle && (
<Typography variant='h3'>
Allowed Domains
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={'Your chatbot will only work when used from the following domains.'}
/>
</Typography>
)}
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<Stack direction='column' spacing={2}>
<Typography>Domains</Typography>
@@ -193,16 +195,19 @@ const AllowedDomains = ({ dialogProps, onConfirm }) => {
/>
</Stack>
</Stack>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</Stack>
)
}
AllowedDomains.propTypes = {
dialogProps: PropTypes.object,
onConfirm: PropTypes.func
onConfirm: PropTypes.func,
hideTitle: PropTypes.bool
}
export default AllowedDomains
@@ -15,6 +15,7 @@ import {
ListItemAvatar,
ListItemText
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { IconX } from '@tabler/icons-react'
@@ -221,6 +222,7 @@ const analyticProviders = [
const AnalyseFlow = ({ dialogProps }) => {
const dispatch = useDispatch()
const theme = useTheme()
useNotifier()
@@ -339,7 +341,12 @@ const AnalyseFlow = ({ dialogProps }) => {
sx={{ ml: 1 }}
primary={provider.label}
secondary={
<a target='_blank' rel='noreferrer' href={provider.url}>
<a
target='_blank'
rel='noreferrer'
href={provider.url}
style={{ color: theme.palette.primary.main, opacity: 0.85 }}
>
{provider.url}
</a>
}
@@ -416,9 +423,11 @@ const AnalyseFlow = ({ dialogProps }) => {
</AccordionDetails>
</Accordion>
))}
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={onSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</>
)
}
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
// material-ui
import { Button, Box } from '@mui/material'
import { Button, Box, Stack } from '@mui/material'
import { IconX } from '@tabler/icons-react'
// Project import
@@ -91,14 +91,14 @@ const ChatFeedback = ({ dialogProps, onConfirm }) => {
}, [dialogProps])
return (
<>
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<SwitchInput label='Enable chat feedback' onChange={handleChange} value={chatFeedbackStatus} />
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<SwitchInput label='Enable chat feedback' onChange={handleChange} value={chatFeedbackStatus} />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={onSave}>
Save
</StyledButton>
</>
</Stack>
)
}
@@ -5,7 +5,19 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import parser from 'html-react-parser'
// material-ui
import { Button, Box, Typography, FormControl, RadioGroup, FormControlLabel, Radio } from '@mui/material'
import {
Button,
Box,
Typography,
FormControl,
RadioGroup,
FormControlLabel,
Radio,
Accordion,
AccordionSummary,
AccordionDetails
} from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { IconX, IconBulb } from '@tabler/icons-react'
// Project import
@@ -20,7 +32,7 @@ import chatflowsApi from '@/api/chatflows'
const message = `The full contents of uploaded files will be converted to text and sent to the Agent.
<br />
Refer <a href='https://docs.flowiseai.com/using-flowise/uploads#files' target='_blank'>docs</a> for more details.`
Refer <a href='https://docs.flowiseai.com/using-flowise/uploads#files' target='_blank' style='color: #2196f3'>docs</a> for more details.`
const availableFileTypes = [
{ name: 'CSS', ext: 'text/css', extension: '.css' },
@@ -52,8 +64,6 @@ const FileUpload = ({ dialogProps }) => {
const [allowedFileTypes, setAllowedFileTypes] = useState([])
const [chatbotConfig, setChatbotConfig] = useState({})
const [pdfUsage, setPdfUsage] = useState('perPage')
const [pdfLegacyBuild, setPdfLegacyBuild] = useState(false)
const handleChange = (value) => {
setFullFileUpload(value)
}
@@ -71,18 +81,13 @@ const FileUpload = ({ dialogProps }) => {
setPdfUsage(event.target.value)
}
const handleLegacyBuildChange = (value) => {
setPdfLegacyBuild(value)
}
const onSave = async () => {
try {
const value = {
status: fullFileUpload,
allowedUploadFileTypes: allowedFileTypes.join(','),
pdfFile: {
usage: pdfUsage,
legacyBuild: pdfLegacyBuild
usage: pdfUsage
}
}
chatbotConfig.fullFileUpload = value
@@ -140,13 +145,8 @@ const FileUpload = ({ dialogProps }) => {
const allowedFileTypes = chatbotConfig.fullFileUpload.allowedUploadFileTypes.split(',')
setAllowedFileTypes(allowedFileTypes)
}
if (chatbotConfig.fullFileUpload?.pdfFile) {
if (chatbotConfig.fullFileUpload.pdfFile.usage) {
setPdfUsage(chatbotConfig.fullFileUpload.pdfFile.usage)
}
if (chatbotConfig.fullFileUpload.pdfFile.legacyBuild !== undefined) {
setPdfLegacyBuild(chatbotConfig.fullFileUpload.pdfFile.legacyBuild)
}
if (chatbotConfig.fullFileUpload?.pdfFile?.usage) {
setPdfUsage(chatbotConfig.fullFileUpload.pdfFile.usage)
}
} catch (e) {
setChatbotConfig({})
@@ -170,31 +170,27 @@ const FileUpload = ({ dialogProps }) => {
mb: 2
}}
>
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
alignItems: 'center',
gap: 1.25,
borderRadius: '8px',
bgcolor: 'rgba(34, 197, 94, 0.08)',
border: '1px solid',
borderColor: 'rgba(34, 197, 94, 0.2)',
width: '100%',
padding: 10
px: 1.75,
py: 1.25
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>{parser(message)}</span>
</div>
</div>
<IconBulb size={20} color='#16a34a' style={{ flexShrink: 0 }} />
<Typography sx={{ color: 'text.secondary', fontSize: '0.8125rem', lineHeight: 1.5 }}>{parser(message)}</Typography>
</Box>
<SwitchInput label='Enable Full File Upload' onChange={handleChange} value={fullFileUpload} />
</Box>
<Typography sx={{ fontSize: 14, fontWeight: 500, marginBottom: 1 }}>Allow Uploads of Type</Typography>
<Typography sx={{ fontSize: '0.875rem', fontWeight: 500, mb: 1 }}>Allow Uploads of Type</Typography>
<div
style={{
display: 'grid',
@@ -224,54 +220,65 @@ const FileUpload = ({ dialogProps }) => {
disabled={!fullFileUpload}
onChange={handleAllowedFileTypesChange}
/>
<label htmlFor={fileType.ext} style={{ marginLeft: 10 }}>
<label htmlFor={fileType.ext} style={{ marginLeft: 10, fontSize: '0.8125rem' }}>
{fileType.name} ({fileType.extension})
</label>
</div>
))}
</div>
{allowedFileTypes.includes('application/pdf') && fullFileUpload && (
<Box
{fullFileUpload && (
<Accordion
disableGutters
elevation={0}
sx={{
borderRadius: 2,
border: customization.isDarkMode ? '1px solid #424242' : '1px solid #e0e0e0',
backgroundColor: customization.isDarkMode ? '#2d2d2d' : '#fafafa',
padding: 3,
marginBottom: 3,
marginTop: 2
mt: 2,
borderRadius: '8px !important',
border: '1px solid',
borderColor: 'divider',
bgcolor: customization.isDarkMode ? 'rgba(255,255,255,0.02)' : 'grey.50',
'&:before': { display: 'none' },
overflow: 'hidden'
}}
>
<Typography
sx={{ fontSize: 16, fontWeight: 600, marginBottom: 2, color: customization.isDarkMode ? '#ffffff' : '#424242' }}
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: '1.1rem', color: 'text.secondary' }} />}
sx={{ minHeight: 40, px: 2, '& .MuiAccordionSummary-content': { my: 0.75 } }}
>
PDF Configuration
</Typography>
<Box>
<Typography sx={{ fontSize: 14, fontWeight: 500, marginBottom: 1 }}>PDF Usage</Typography>
<FormControl disabled={!fullFileUpload}>
<RadioGroup name='pdf-usage' value={pdfUsage} onChange={handlePdfUsageChange}>
<FormControlLabel value='perPage' control={<Radio />} label='One document per page' />
<FormControlLabel value='perFile' control={<Radio />} label='One document per file' />
</RadioGroup>
</FormControl>
</Box>
<Box>
<SwitchInput
label='Use Legacy Build (for PDF compatibility issues)'
onChange={handleLegacyBuildChange}
value={pdfLegacyBuild}
disabled={!fullFileUpload}
/>
</Box>
</Box>
<Typography sx={{ fontSize: '0.8125rem', fontWeight: 500, color: 'text.secondary' }}>Advanced Settings</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 2, pt: 0, pb: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* PDF Processing */}
{allowedFileTypes.includes('application/pdf') && (
<Box>
<Typography sx={{ fontSize: '0.8125rem', fontWeight: 500, color: 'text.primary', mb: 0.75 }}>
PDF Processing
</Typography>
<FormControl disabled={!fullFileUpload}>
<RadioGroup name='pdf-usage' value={pdfUsage} onChange={handlePdfUsageChange}>
<FormControlLabel
value='perPage'
control={<Radio size='small' />}
label={<Typography sx={{ fontSize: '0.8125rem' }}>One document per page</Typography>}
/>
<FormControlLabel
value='perFile'
control={<Radio size='small' />}
label={<Typography sx={{ fontSize: '0.8125rem' }}>One document per file</Typography>}
/>
</RadioGroup>
</FormControl>
</Box>
)}
</AccordionDetails>
</Accordion>
)}
<StyledButton style={{ marginBottom: 10, marginTop: 20 }} variant='contained' onClick={onSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</>
)
}
@@ -601,9 +601,11 @@ const FollowUpPrompts = ({ dialogProps }) => {
</>
)}
</Box>
<StyledButton disabled={checkDisabled()} variant='contained' onClick={onSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton disabled={checkDisabled()} variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</>
)
}
@@ -160,14 +160,16 @@ const Leads = ({ dialogProps }) => {
</>
)}
</Box>
<StyledButton
disabled={!leadsConfig['name'] && !leadsConfig['phone'] && !leadsConfig['email'] && leadsConfig['status']}
style={{ marginBottom: 10, marginTop: 10 }}
variant='contained'
onClick={onSave}
>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton
disabled={!leadsConfig['name'] && !leadsConfig['phone'] && !leadsConfig['email'] && leadsConfig['status']}
variant='contained'
onClick={onSave}
sx={{ minWidth: 100 }}
>
Save
</StyledButton>
</Box>
</>
)
}
@@ -23,7 +23,7 @@ import useNotifier from '@/utils/useNotifier'
import mcpServerApi from '@/api/mcpserver'
import chatflowsApi from '@/api/chatflows'
const McpServer = ({ dialogProps }) => {
const McpServer = ({ dialogProps, onStatusChange }) => {
const dispatch = useDispatch()
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@@ -123,6 +123,7 @@ const McpServer = ({ dialogProps }) => {
setToken(resp.data.token || '')
setToolName(resp.data.toolName || '')
setDescription(resp.data.description || '')
onStatusChange?.(resp.data.enabled)
showSuccess('MCP Server settings saved')
}
} else {
@@ -136,12 +137,14 @@ const McpServer = ({ dialogProps }) => {
setToolName(resp.data.toolName || '')
setDescription(resp.data.description || '')
setHasExistingConfig(true)
onStatusChange?.(resp.data.enabled)
showSuccess('MCP Server settings saved')
}
}
} else {
await mcpServerApi.deleteMcpServerConfig(dialogProps.chatflow.id)
setMcpEnabled(false)
onStatusChange?.(false)
showSuccess('MCP Server disabled')
}
await refreshChatflowStore()
@@ -215,13 +218,15 @@ const McpServer = ({ dialogProps }) => {
useEffect(() => {
if (getMcpServerConfigApi.data) {
setMcpEnabled(getMcpServerConfigApi.data.enabled || false)
const enabled = getMcpServerConfigApi.data.enabled || false
setMcpEnabled(enabled)
setToolName(getMcpServerConfigApi.data.toolName || '')
setDescription(getMcpServerConfigApi.data.description || '')
setToken(getMcpServerConfigApi.data.token || '')
setHasExistingConfig(!!getMcpServerConfigApi.data.token)
onStatusChange?.(enabled)
}
}, [getMcpServerConfigApi.data])
}, [getMcpServerConfigApi.data]) // eslint-disable-line react-hooks/exhaustive-deps
if (getMcpServerConfigApi.loading) {
return (
@@ -391,20 +396,23 @@ const McpServer = ({ dialogProps }) => {
</Box>
)}
<StyledButton
style={{ marginBottom: 10, marginTop: 20 }}
variant='contained'
disabled={loading || (mcpEnabled && (!!toolNameError || !toolName.trim() || !description.trim()))}
onClick={onSave}
>
{loading ? 'Saving...' : 'Save'}
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton
variant='contained'
disabled={loading || (mcpEnabled && (!!toolNameError || !toolName.trim() || !description.trim()))}
onClick={onSave}
sx={{ minWidth: 100 }}
>
{loading ? 'Saving...' : 'Save'}
</StyledButton>
</Box>
</>
)
}
McpServer.propTypes = {
dialogProps: PropTypes.object
dialogProps: PropTypes.object,
onStatusChange: PropTypes.func
}
export default McpServer
@@ -5,6 +5,7 @@ import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Paper,
Stack,
@@ -17,7 +18,6 @@ import {
Typography,
Card
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
// Project import
import { StyledButton } from '@/ui-component/button/StyledButton'
@@ -39,6 +39,9 @@ import variablesApi from '@/api/variables'
// utils
const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
const customization = useSelector((state) => state.customization)
const isDark = customization?.isDarkMode
const handleChange = (enabled, row) => {
onToggle(row, enabled)
}
@@ -47,10 +50,8 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
if (key === 'enabled') {
return <SwitchInput onChange={(enabled) => handleChange(enabled, row)} value={row.enabled} />
} else if (key === 'type' && row.schema) {
// If there's schema information, add a tooltip
let schemaContent
if (Array.isArray(row.schema)) {
// Handle array format: [{ name: "field", type: "string" }, ...]
schemaContent =
'[<br>' +
row.schema
@@ -67,30 +68,53 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
.join(',<br>') +
'<br>]'
} else if (typeof row.schema === 'object' && row.schema !== null) {
// Handle object format: { "field": "string", "field2": "number", ... }
schemaContent = JSON.stringify(row.schema, null, 2).replace(/\n/g, '<br>').replace(/ /g, '&nbsp;')
} else {
schemaContent = 'No schema available'
}
return (
<Stack direction='row' alignItems='center' spacing={1}>
<Typography>{row[key]}</Typography>
<Stack direction='row' alignItems='center' spacing={0.5}>
<Typography sx={{ fontSize: '0.8rem' }}>{row[key]}</Typography>
<TooltipWithParser title={`<div>Schema:<br/>${schemaContent}</div>`} />
</Stack>
)
} else {
return row[key]
return <Typography sx={{ fontSize: '0.8rem', wordBreak: 'break-all' }}>{row[key]}</Typography>
}
}
const columnLabels = { label: 'Label', name: 'Name', type: 'Type', enabled: 'On' }
return (
<TableContainer component={Paper}>
<Table size='small' sx={{ minWidth: 650, ...sx }} aria-label='simple table'>
<TableContainer
component={Paper}
elevation={0}
sx={{
borderRadius: 0,
boxShadow: 'none',
bgcolor: 'transparent'
}}
>
<Table size='small' sx={{ ...sx }} aria-label='override config table'>
<TableHead>
<TableRow>
{columns.map((col, index) => (
<TableCell key={index}>{col.charAt(0).toUpperCase() + col.slice(1)}</TableCell>
<TableCell
key={index}
sx={{
fontSize: '0.8125rem',
fontWeight: 600,
color: 'text.secondary',
py: 1.5,
borderBottom: '1px solid',
borderColor: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)',
...(col === 'enabled' ? { width: 50, textAlign: 'center' } : {}),
...(col === 'type' ? { width: 100 } : {})
}}
>
{columnLabels[col] || col.charAt(0).toUpperCase() + col.slice(1)}
</TableCell>
))}
</TableRow>
</TableHead>
@@ -99,7 +123,19 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{Object.keys(row).map((key, index) => {
if (key !== 'id' && key !== 'schema') {
return <TableCell key={index}>{renderCellContent(key, row)}</TableCell>
return (
<TableCell
key={index}
sx={{
py: 1.5,
borderBottom: '1px solid',
borderColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)',
...(key === 'enabled' ? { textAlign: 'center' } : {})
}}
>
{renderCellContent(key, row)}
</TableCell>
)
}
})}
</TableRow>
@@ -117,14 +153,14 @@ OverrideConfigTable.propTypes = {
onToggle: PropTypes.func
}
const OverrideConfig = ({ dialogProps }) => {
const OverrideConfig = ({ dialogProps, hideTitle = false }) => {
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const chatflow = useSelector((state) => state.canvas.chatflow)
const chatflowid = chatflow.id
const apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {}
useNotifier()
const theme = useTheme()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
@@ -359,22 +395,32 @@ const OverrideConfig = ({ dialogProps }) => {
}, [getAllVariablesApi.data])
return (
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Override Configuration
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={
'Enable or disable which properties of the flow configuration can be overridden. Refer to the <a href="https://docs.flowiseai.com/using-flowise/prediction#configuration-override" target="_blank">documentation</a> for more information.'
}
/>
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{!hideTitle && (
<Typography variant='h3'>
Override Configuration
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={
'Enable or disable which properties of the flow configuration can be overridden. Refer to the <a href="https://docs.flowiseai.com/using-flowise/prediction#configuration-override" target="_blank">documentation</a> for more information.'
}
/>
</Typography>
)}
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<SwitchInput label='Enable Override Configuration' onChange={setOverrideConfigStatus} value={overrideConfigStatus} />
{overrideConfigStatus && (
<>
{nodeOverrides && nodeConfig && (
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Card
elevation={0}
sx={{
borderRadius: '8px',
border: '1px solid',
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)',
p: 2
}}
>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconBox />
<Typography variant='h4'>Nodes</Typography>
@@ -388,6 +434,11 @@ const OverrideConfig = ({ dialogProps }) => {
onChange={handleAccordionChange(nodeLabel)}
key={nodeLabel}
disableGutters
sx={{
'&:before': {
bgcolor: customization.isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)'
}
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
@@ -444,7 +495,15 @@ const OverrideConfig = ({ dialogProps }) => {
</Card>
)}
{variableOverrides && variableOverrides.length > 0 && (
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Card
elevation={0}
sx={{
borderRadius: '8px',
border: '1px solid',
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)',
p: 2
}}
>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconVariable />
<Typography variant='h4'>Variables</Typography>
@@ -459,15 +518,18 @@ const OverrideConfig = ({ dialogProps }) => {
</>
)}
</Stack>
<StyledButton variant='contained' onClick={onOverrideConfigSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onOverrideConfigSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</Stack>
)
}
OverrideConfig.propTypes = {
dialogProps: PropTypes.object
dialogProps: PropTypes.object,
hideTitle: PropTypes.bool
}
export default OverrideConfig
@@ -179,7 +179,7 @@ const PostProcessing = ({ dialogProps }) => {
style={{
marginTop: '10px',
border: '1px solid',
borderColor: theme.palette.grey['300'],
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : theme.palette.grey['300'],
borderRadius: '6px',
height: '200px',
width: '100%'
@@ -196,7 +196,16 @@ const PostProcessing = ({ dialogProps }) => {
/>
</div>
</Box>
<Card sx={{ borderColor: theme.palette.primary[200] + 75, mt: 2, mb: 2 }} variant='outlined'>
<Card
elevation={0}
sx={{
borderRadius: '8px',
border: '1px solid',
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)',
mt: 2,
mb: 2
}}
>
<Accordion
disableGutters
sx={{
@@ -206,19 +215,61 @@ const PostProcessing = ({ dialogProps }) => {
}}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Available Variables</Typography>
<Typography sx={{ fontSize: '0.875rem', fontWeight: 500 }}>Available Variables</Typography>
</AccordionSummary>
<AccordionDetails sx={{ p: 0 }}>
<TableContainer component={Paper}>
<Table aria-label='available variables table'>
<TableContainer component={Paper} elevation={0} sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
<Table size='small' aria-label='available variables table'>
<TableHead>
<TableRow>
<TableCell sx={{ width: '30%' }}>Variable</TableCell>
<TableCell sx={{ width: '15%' }}>Type</TableCell>
<TableCell sx={{ width: '55%' }}>Description</TableCell>
<TableCell
sx={{
width: '30%',
fontSize: '0.8125rem',
fontWeight: 600,
color: 'text.secondary',
py: 1.5,
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'
}}
>
Variable
</TableCell>
<TableCell
sx={{
width: '15%',
fontSize: '0.8125rem',
fontWeight: 600,
color: 'text.secondary',
py: 1.5,
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'
}}
>
Type
</TableCell>
<TableCell
sx={{
width: '55%',
fontSize: '0.8125rem',
fontWeight: 600,
color: 'text.secondary',
py: 1.5,
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'
}}
>
Description
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableBody
sx={{
'& td': {
fontSize: '0.8rem',
py: 1.5,
borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)'
},
'& tr:last-child td': { border: 0 }
}}
>
<TableRow>
<TableCell>
<code>$flow.rawOutput</code>
@@ -283,11 +334,11 @@ const PostProcessing = ({ dialogProps }) => {
<TableCell>List of artifacts generated during execution</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ borderBottom: 'none' }}>
<TableCell>
<code>$flow.fileAnnotations</code>
</TableCell>
<TableCell sx={{ borderBottom: 'none' }}>array</TableCell>
<TableCell sx={{ borderBottom: 'none' }}>File annotations associated with the response</TableCell>
<TableCell>array</TableCell>
<TableCell>File annotations associated with the response</TableCell>
</TableRow>
</TableBody>
</Table>
@@ -295,14 +346,16 @@ const PostProcessing = ({ dialogProps }) => {
</AccordionDetails>
</Accordion>
</Card>
<StyledButton
style={{ marginBottom: 10, marginTop: 10 }}
variant='contained'
disabled={!postProcessingFunction || postProcessingFunction?.trim().length === 0}
onClick={onSave}
>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton
variant='contained'
disabled={!postProcessingFunction || postProcessingFunction?.trim().length === 0}
onClick={onSave}
sx={{ minWidth: 100 }}
>
Save
</StyledButton>
</Box>
<ExpandTextDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions'
import PropTypes from 'prop-types'
import { Typography, Button, OutlinedInput, Stack } from '@mui/material'
import { Typography, Button, OutlinedInput, Stack, Box } from '@mui/material'
// Project import
import { StyledButton } from '@/ui-component/button/StyledButton'
@@ -19,7 +19,7 @@ import chatflowsApi from '@/api/chatflows'
// utils
import useNotifier from '@/utils/useNotifier'
const RateLimit = ({ dialogProps }) => {
const RateLimit = ({ dialogProps, hideTitle = false }) => {
const dispatch = useDispatch()
const chatflow = useSelector((state) => state.canvas.chatflow)
const chatflowid = chatflow.id
@@ -147,36 +147,39 @@ const RateLimit = ({ dialogProps }) => {
}
return (
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Rate Limit{' '}
<TooltipWithParser
style={{ marginLeft: 10 }}
title={
'Visit <a target="_blank" href="https://docs.flowiseai.com/configuration/rate-limit">Rate Limit Setup Guide</a> to set up Rate Limit correctly in your hosting environment.'
}
/>
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<SwitchInput label='Enable Rate Limit' onChange={handleChange} value={rateLimitStatus} />
{rateLimitStatus && (
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')}
{textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')}
{textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')}
</Stack>
)}
</Stack>
<StyledButton disabled={checkDisabled()} variant='contained' onClick={() => onSave()} sx={{ width: 'auto' }}>
Save
</StyledButton>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{!hideTitle && (
<Typography variant='h3'>
Rate Limit{' '}
<TooltipWithParser
style={{ marginLeft: 10 }}
title={
'Visit <a target="_blank" href="https://docs.flowiseai.com/configuration/rate-limit">Rate Limit Setup Guide</a> to set up Rate Limit correctly in your hosting environment.'
}
/>
</Typography>
)}
<SwitchInput label='Enable Rate Limit' onChange={handleChange} value={rateLimitStatus} />
{rateLimitStatus && (
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')}
{textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')}
{textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')}
</Stack>
)}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton disabled={checkDisabled()} variant='contained' onClick={() => onSave()} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</Stack>
)
}
RateLimit.propTypes = {
isSessionMemory: PropTypes.bool,
dialogProps: PropTypes.object
dialogProps: PropTypes.object,
hideTitle: PropTypes.bool
}
export default RateLimit
@@ -478,14 +478,16 @@ const SpeechToText = ({ dialogProps, onConfirm }) => {
))}
</>
)}
<StyledButton
style={{ marginBottom: 10, marginTop: 10 }}
disabled={selectedProvider !== 'none' && !speechToText[selectedProvider]?.credentialId}
variant='contained'
onClick={onSave}
>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton
disabled={selectedProvider !== 'none' && !speechToText[selectedProvider]?.credentialId}
variant='contained'
onClick={onSave}
sx={{ minWidth: 100 }}
>
Save
</StyledButton>
</Box>
</>
)
}
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions'
// material-ui
import { Button, IconButton, OutlinedInput, Box, List, InputAdornment } from '@mui/material'
import { Button, IconButton, OutlinedInput, Box, List, InputAdornment, Typography } from '@mui/material'
import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons-react'
// Project import
@@ -133,28 +133,24 @@ const StarterPrompts = ({ dialogProps, onConfirm }) => {
return (
<>
<div
style={{
<Box
sx={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
padding: 10
alignItems: 'center',
gap: 1.25,
borderRadius: '8px',
bgcolor: 'rgba(34, 197, 94, 0.08)',
border: '1px solid',
borderColor: 'rgba(34, 197, 94, 0.2)',
px: 1.75,
py: 1.25
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
Starter prompts will only be shown when there is no messages on the chat
</span>
</div>
</div>
<IconBulb size={20} color='#16a34a' style={{ flexShrink: 0 }} />
<Typography sx={{ color: 'text.secondary', fontSize: '0.8125rem', lineHeight: 1.5 }}>
Starter prompts will only be shown when there are no messages on the chat
</Typography>
</Box>
<Box sx={{ '& > :not(style)': { m: 1 }, pt: 2 }}>
<List>
{inputFields.map((data, index) => {
@@ -199,9 +195,11 @@ const StarterPrompts = ({ dialogProps, onConfirm }) => {
})}
</List>
</Box>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton variant='contained' onClick={onSave} sx={{ minWidth: 100 }}>
Save
</StyledButton>
</Box>
</>
)
}
@@ -641,14 +641,16 @@ const TextToSpeech = ({ dialogProps }) => {
</Box>
</>
)}
<StyledButton
style={{ marginBottom: 10, marginTop: 10 }}
disabled={selectedProvider !== 'none' && !textToSpeech?.[selectedProvider]?.credentialId}
variant='contained'
onClick={onSave}
>
Save
</StyledButton>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%', mt: 2 }}>
<StyledButton
disabled={selectedProvider !== 'none' && !textToSpeech?.[selectedProvider]?.credentialId}
variant='contained'
onClick={onSave}
sx={{ minWidth: 100 }}
>
Save
</StyledButton>
</Box>
</>
)
}