mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
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:
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -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, ' ')
|
||||
} 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user