mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Feature: Custom Templates (#3169)
* New Feature: Custom Templates in the marketplace. * New Feature: Custom Templates in the marketplace. * Custom Template Delete and Shortcut in the dropdown menu * auto detect framework * minor ui fixes * adding custom template feature for tools * ui tool dialog save template --------- Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt'
|
||||
import ThumbsUpDownOutlinedIcon from '@mui/icons-material/ThumbsUpDownOutlined'
|
||||
import VpnLockOutlinedIcon from '@mui/icons-material/VpnLockOutlined'
|
||||
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'
|
||||
import ExportTemplateOutlinedIcon from '@mui/icons-material/BookmarksOutlined'
|
||||
import Button from '@mui/material/Button'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
@@ -35,6 +36,7 @@ import useNotifier from '@/utils/useNotifier'
|
||||
import ChatFeedbackDialog from '../dialog/ChatFeedbackDialog'
|
||||
import AllowedDomainsDialog from '../dialog/AllowedDomainsDialog'
|
||||
import SpeechToTextDialog from '../dialog/SpeechToTextDialog'
|
||||
import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog'
|
||||
|
||||
const StyledMenu = styled((props) => (
|
||||
<Menu
|
||||
@@ -95,6 +97,9 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
const [speechToTextDialogOpen, setSpeechToTextDialogOpen] = useState(false)
|
||||
const [speechToTextDialogProps, setSpeechToTextDialogProps] = useState({})
|
||||
|
||||
const [exportTemplateDialogOpen, setExportTemplateDialogOpen] = useState(false)
|
||||
const [exportTemplateDialogProps, setExportTemplateDialogProps] = useState({})
|
||||
|
||||
const title = isAgentCanvas ? 'Agents' : 'Chatflow'
|
||||
|
||||
const handleClick = (event) => {
|
||||
@@ -119,6 +124,14 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
setConversationStartersDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleExportTemplate = () => {
|
||||
setAnchorEl(null)
|
||||
setExportTemplateDialogProps({
|
||||
chatflow: chatflow
|
||||
})
|
||||
setExportTemplateDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleFlowChatFeedback = () => {
|
||||
setAnchorEl(null)
|
||||
setChatFeedbackDialogProps({
|
||||
@@ -306,6 +319,10 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
<FileDownloadIcon />
|
||||
Export
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleExportTemplate} disableRipple>
|
||||
<ExportTemplateOutlinedIcon />
|
||||
Save As Template
|
||||
</MenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
|
||||
<PictureInPictureAltIcon />
|
||||
@@ -369,6 +386,13 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
dialogProps={speechToTextDialogProps}
|
||||
onCancel={() => setSpeechToTextDialogOpen(false)}
|
||||
/>
|
||||
{exportTemplateDialogOpen && (
|
||||
<ExportAsTemplateDialog
|
||||
show={exportTemplateDialogOpen}
|
||||
dialogProps={exportTemplateDialogProps}
|
||||
onCancel={() => setExportTemplateDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, OutlinedInput, Typography } from '@mui/material'
|
||||
|
||||
// store
|
||||
import {
|
||||
closeSnackbar as closeSnackbarAction,
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
HIDE_CANVAS_DIALOG,
|
||||
SHOW_CANVAS_DIALOG
|
||||
} from '@/store/actions'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import marketplacesApi from '@/api/marketplaces'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// Project imports
|
||||
|
||||
const ExportAsTemplateDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
const [name, setName] = useState('')
|
||||
const [flowType, setFlowType] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [badge, setBadge] = useState('')
|
||||
const [usecases, setUsecases] = useState([])
|
||||
const [usecaseInput, setUsecaseInput] = useState('')
|
||||
|
||||
const saveCustomTemplateApi = useApi(marketplacesApi.saveAsCustomTemplate)
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
useNotifier()
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.chatflow) {
|
||||
setName(dialogProps.chatflow.name)
|
||||
setFlowType(dialogProps.chatflow.type === 'MULTIAGENT' ? 'Agentflow' : 'Chatflow')
|
||||
}
|
||||
|
||||
if (dialogProps.tool) {
|
||||
setName(dialogProps.tool.name)
|
||||
setDescription(dialogProps.tool.description)
|
||||
setFlowType('Tool')
|
||||
}
|
||||
|
||||
return () => {
|
||||
setName('')
|
||||
setDescription('')
|
||||
setBadge('')
|
||||
setUsecases([])
|
||||
setFlowType('')
|
||||
setUsecaseInput('')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const handleUsecaseInputChange = (event) => {
|
||||
setUsecaseInput(event.target.value)
|
||||
}
|
||||
|
||||
const handleUsecaseInputKeyDown = (event) => {
|
||||
if (event.key === 'Enter' && usecaseInput.trim()) {
|
||||
event.preventDefault()
|
||||
if (!usecases.includes(usecaseInput)) {
|
||||
setUsecases([...usecases, usecaseInput])
|
||||
setUsecaseInput('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUsecaseDelete = (toDelete) => {
|
||||
setUsecases(usecases.filter((category) => category !== toDelete))
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
if (name.trim() === '') {
|
||||
enqueueSnackbar({
|
||||
message: 'Template Name is mandatory!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const template = {
|
||||
name,
|
||||
description,
|
||||
badge: badge ? badge.toUpperCase() : undefined,
|
||||
usecases,
|
||||
type: flowType
|
||||
}
|
||||
if (dialogProps.chatflow) {
|
||||
template.chatflowId = dialogProps.chatflow.id
|
||||
}
|
||||
if (dialogProps.tool) {
|
||||
template.tool = {
|
||||
iconSrc: dialogProps.tool.iconSrc,
|
||||
schema: dialogProps.tool.schema,
|
||||
func: dialogProps.tool.func
|
||||
}
|
||||
}
|
||||
saveCustomTemplateApi.request(template)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (saveCustomTemplateApi.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Saved as template successfully!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveCustomTemplateApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (saveCustomTemplateApi.error) {
|
||||
enqueueSnackbar({
|
||||
message: 'Failed to save as template!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveCustomTemplateApi.error])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title || 'Export As Template'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
Name<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<OutlinedInput
|
||||
id={'name'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
value={name}
|
||||
name='name'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Description</Typography>
|
||||
<OutlinedInput
|
||||
id={'description'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
value={description}
|
||||
name='description'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Badge</Typography>
|
||||
<OutlinedInput
|
||||
id={'badge'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
value={badge}
|
||||
name='badge'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setBadge(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Usecases</Typography>
|
||||
{usecases.length > 0 && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
{usecases.map((uc, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={uc}
|
||||
onDelete={() => handleUsecaseDelete(uc)}
|
||||
style={{ marginRight: 5, marginBottom: 5 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<OutlinedInput
|
||||
fullWidth
|
||||
value={usecaseInput}
|
||||
onChange={handleUsecaseInputChange}
|
||||
onKeyDown={handleUsecaseInputKeyDown}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography variant='body2' sx={{ fontStyle: 'italic', mt: 1 }} color='text.secondary'>
|
||||
Type a usecase and press enter to add it to the list. You can add as many items as you want.
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName || 'Cancel'}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={onConfirm}>
|
||||
{dialogProps.confirmButtonName || 'Save Template'}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ExportAsTemplateDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExportAsTemplateDialog
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
TableRow,
|
||||
Typography,
|
||||
Stack,
|
||||
useTheme
|
||||
useTheme,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { IconTrash } from '@tabler/icons-react'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
@@ -46,7 +48,8 @@ export const MarketplaceTable = ({
|
||||
filterByUsecases,
|
||||
goToCanvas,
|
||||
goToTool,
|
||||
isLoading
|
||||
isLoading,
|
||||
onDelete
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
@@ -87,6 +90,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell component='th' scope='row' key='6'>
|
||||
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell component='th' scope='row' key='7'>
|
||||
Delete
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -114,6 +122,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
@@ -137,6 +150,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
@@ -234,6 +252,13 @@ export const MarketplaceTable = ({
|
||||
))}
|
||||
</Typography>
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell key='7'>
|
||||
<IconButton title='Delete' color='error' onClick={() => onDelete(row)}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
@@ -254,5 +279,6 @@ MarketplaceTable.propTypes = {
|
||||
filterByUsecases: PropTypes.func,
|
||||
goToTool: PropTypes.func,
|
||||
goToCanvas: PropTypes.func,
|
||||
isLoading: PropTypes.bool
|
||||
isLoading: PropTypes.bool,
|
||||
onDelete: PropTypes.func
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user