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:
Vinod Kiran
2024-09-16 19:14:39 +05:30
committed by GitHub
parent 44b70ca7e2
commit b02bdc74ad
23 changed files with 1217 additions and 170 deletions
@@ -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' }}>&nbsp;*</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'>
&nbsp;
</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
}