add custom tool

This commit is contained in:
Henry
2023-06-21 18:31:53 +01:00
parent 8c880199cd
commit 70da39629c
28 changed files with 1346 additions and 43 deletions
+19
View File
@@ -0,0 +1,19 @@
import client from './client'
const getAllTools = () => client.get('/tools')
const getSpecificTool = (id) => client.get(`/tools/${id}`)
const createNewTool = (body) => client.post(`/tools`, body)
const updateTool = (id, body) => client.put(`/tools/${id}`, body)
const deleteTool = (id) => client.delete(`/tools/${id}`)
export default {
getAllTools,
getSpecificTool,
createNewTool,
updateTool,
deleteTool
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -27,6 +27,14 @@ const dashboard = {
icon: icons.IconBuildingStore,
breadcrumbs: true
},
{
id: 'tools',
title: 'Tools',
type: 'item',
url: '/tools',
icon: icons.IconTool,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
+7
View File
@@ -13,6 +13,9 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// apikey routing
const APIKey = Loadable(lazy(() => import('views/apikey')))
// apikey routing
const Tools = Loadable(lazy(() => import('views/tools')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -34,6 +37,10 @@ const MainRoutes = {
{
path: '/apikey',
element: <APIKey />
},
{
path: '/tools',
element: <Tools />
}
]
}
+22 -24
View File
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { Box, Grid, Chip, Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
import { Box, Grid, Typography } from '@mui/material'
// project imports
import MainCard from 'ui-component/cards/MainCard'
@@ -27,20 +27,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
// ===========================|| CONTRACT CARD ||=========================== //
const ItemCard = ({ isLoading, data, images, onClick }) => {
const theme = useTheme()
const chipSX = {
height: 24,
padding: '0 6px'
}
const activeChatflowSX = {
...chipSX,
color: 'white',
backgroundColor: theme.palette.success.dark
}
const ItemCard = ({ isLoading, data, images, color, onClick }) => {
return (
<>
{isLoading ? (
@@ -49,7 +36,24 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
<CardWrapper border={false} content={false} onClick={onClick}>
<Box sx={{ p: 2.25 }}>
<Grid container direction='column'>
<div>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
{color && (
<div
style={{
width: 35,
height: 35,
marginRight: 10,
borderRadius: '50%',
background: color
}}
></div>
)}
<Typography
sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
>
@@ -61,13 +65,6 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
{data.description}
</span>
)}
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
{data.deployed && (
<Grid item>
<Chip label='Deployed' sx={activeChatflowSX} />
</Grid>
)}
</Grid>
{images && (
<div
style={{
@@ -110,6 +107,7 @@ ItemCard.propTypes = {
isLoading: PropTypes.bool,
data: PropTypes.object,
images: PropTypes.array,
color: PropTypes.string,
onClick: PropTypes.func
}
@@ -0,0 +1,147 @@
import { useState, useEffect, Fragment } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import axios from 'axios'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
import { baseURL } from 'store/constant'
const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
borderRadius: '10px',
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 10,
margin: 10
}
}
})
const fetchList = async ({ name, nodeData }) => {
const loadMethod = nodeData.inputParams.find((param) => param.name === name)?.loadMethod
const username = localStorage.getItem('username')
const password = localStorage.getItem('password')
let lists = await axios
.post(
`${baseURL}/api/v1/node-load-method/${nodeData.name}`,
{ ...nodeData, loadMethod },
{ auth: username && password ? { username, password } : undefined }
)
.then(async function (response) {
return response.data
})
.catch(function (error) {
console.error(error)
})
return lists
}
export const AsyncDropdown = ({
name,
nodeData,
value,
onSelect,
isCreateNewOption,
onCreateNew,
disabled = false,
disableClearable = false
}) => {
const customization = useSelector((state) => state.customization)
const [open, setOpen] = useState(false)
const [options, setOptions] = useState([])
const [loading, setLoading] = useState(false)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
const addNewOption = [{ label: '- Create New -', name: '-create-' }]
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
useEffect(() => {
setLoading(true)
;(async () => {
const fetchData = async () => {
let response = await fetchList({ name, nodeData })
if (isCreateNewOption) setOptions([...response, ...addNewOption])
else setOptions([...response])
setLoading(false)
}
fetchData()
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<>
<Autocomplete
id={name}
disabled={disabled}
disableClearable={disableClearable}
size='small'
sx={{ width: '100%' }}
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
options={options}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
onChange={(e, selection) => {
const value = selection ? selection.name : ''
if (isCreateNewOption && value === '-create-') {
onCreateNew()
} else {
setInternalValue(value)
onSelect(value)
}
}}
PopperComponent={StyledPopper}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
value={internalValue}
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{loading ? <CircularProgress color='inherit' size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='h5'>{option.label}</Typography>
{option.description && (
<Typography sx={{ color: customization.isDarkMode ? '#9e9e9e' : '' }}>{option.description}</Typography>
)}
</div>
</Box>
)}
/>
</>
)
}
AsyncDropdown.propTypes = {
name: PropTypes.string,
nodeData: PropTypes.object,
value: PropTypes.string,
onSelect: PropTypes.func,
onCreateNew: PropTypes.func,
disabled: PropTypes.bool,
disableClearable: PropTypes.bool,
isCreateNewOption: PropTypes.bool
}
@@ -21,6 +21,7 @@ export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, sty
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
tabSize={4}
style={{
...style,
background: theme.palette.codeEditor.main
@@ -21,6 +21,7 @@ export const LightCodeEditor = ({ value, placeholder, disabled = false, type, st
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
tabSize={4}
style={{
...style,
background: theme.palette.card.main
+37
View File
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types'
import { DataGrid } from '@mui/x-data-grid'
import { IconPlus } from '@tabler/icons'
import { Button } from '@mui/material'
export const Grid = ({ columns, rows, style, onRowUpdate, addNewRow }) => {
const handleProcessRowUpdate = (newRow) => {
onRowUpdate(newRow)
return newRow
}
return (
<>
<Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
Add Item
</Button>
{rows && columns && (
<div style={{ marginTop: 10, height: 300, width: '100%', ...style }}>
<DataGrid
processRowUpdate={handleProcessRowUpdate}
onProcessRowUpdateError={(error) => console.error(error)}
rows={rows}
columns={columns}
/>
</div>
)}
</>
)
}
Grid.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
style: PropTypes.any,
addNewRow: PropTypes.func,
onRowUpdate: PropTypes.func
}
+20 -1
View File
@@ -39,7 +39,7 @@ export const initNode = (nodeData, newNodeId) => {
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1
const whitelistTypes = ['options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
for (let i = 0; i < incoming; i += 1) {
const newInput = {
@@ -334,3 +334,22 @@ export const throttle = (func, limit) => {
}
}
}
export const generateRandomGradient = () => {
function randomColor() {
var color = 'rgb('
for (var i = 0; i < 3; i++) {
var random = Math.floor(Math.random() * 256)
color += random
if (i < 2) {
color += ','
}
}
color += ')'
return color
}
var gradient = 'linear-gradient(' + randomColor() + ', ' + randomColor() + ')'
return gradient
}
@@ -7,10 +7,11 @@ import { useSelector } from 'react-redux'
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton } from '@mui/material'
import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize } from '@tabler/icons'
import { IconArrowsMaximize, IconEdit } from '@tabler/icons'
// project import
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
import { Input } from 'ui-component/input/Input'
import { File } from 'ui-component/file/File'
import { SwitchInput } from 'ui-component/switch/Switch'
@@ -18,6 +19,9 @@ import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper'
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import ToolDialog from 'views/tools/ToolDialog'
const EDITABLE_TOOLS = ['selectedTool']
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
[`& .${tooltipClasses.tooltip}`]: {
@@ -36,6 +40,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [position, setPosition] = useState(0)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('')
const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({})
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
@@ -61,6 +68,42 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
data.inputs[inputParamName] = newValue
}
const editAsyncOption = (inputParamName, inputValue) => {
if (inputParamName === 'selectedTool') {
setAsyncOptionEditDialogProps({
title: 'Edit Tool',
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
toolId: inputValue
})
}
setAsyncOptionEditDialog(inputParamName)
}
const addAsyncOption = (inputParamName) => {
if (inputParamName === 'selectedTool') {
setAsyncOptionEditDialogProps({
title: 'Add New Tool',
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add'
})
}
setAsyncOptionEditDialog(inputParamName)
}
const onConfirmAsyncOption = (selectedOptionId = '') => {
if (!selectedOptionId) {
data.inputs[showAsyncOptionDialog] = ''
} else {
data.inputs[showAsyncOptionDialog] = selectedOptionId
setReloadTimestamp(Date.now().toString())
}
setAsyncOptionEditDialogProps({})
setAsyncOptionEditDialog('')
}
useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
@@ -186,12 +229,44 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
)}
{inputParam.type === 'asyncOptions' && (
<>
{data.inputParams.length === 1 && <div style={{ marginTop: 10 }} />}
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
<AsyncDropdown
disabled={disabled}
name={inputParam.name}
nodeData={data}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
isCreateNewOption={EDITABLE_TOOLS.includes(inputParam.name)}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
onCreateNew={() => addAsyncOption(inputParam.name)}
/>
{EDITABLE_TOOLS.includes(inputParam.name) && data.inputs[inputParam.name] && (
<IconButton
title='Edit'
color='primary'
size='small'
onClick={() => editAsyncOption(inputParam.name, data.inputs[inputParam.name])}
>
<IconEdit />
</IconButton>
)}
</div>
</>
)}
</Box>
</>
)}
<ToolDialog
show={EDITABLE_TOOLS.includes(showAsyncOptionDialog)}
dialogProps={asyncOptionEditDialogProps}
onCancel={() => setAsyncOptionEditDialog('')}
onConfirm={onConfirmAsyncOption}
></ToolDialog>
</div>
)
}
+448
View File
@@ -0,0 +1,448 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import { cloneDeep } from 'lodash'
import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
import { Grid } from 'ui-component/grid/Grid'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import { GridActionsCellItem } from '@mui/x-data-grid'
import DeleteIcon from '@mui/icons-material/Delete'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
import { useTheme } from '@mui/material/styles'
// Icons
import { IconX } from '@tabler/icons'
// API
import toolsApi from 'api/tools'
// Hooks
import useConfirm from 'hooks/useConfirm'
import useApi from 'hooks/useApi'
// utils
import useNotifier from 'utils/useNotifier'
import { generateRandomGradient } from 'utils/genericHelper'
const exampleAPIFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
* Must return a string value at the end of function
*/
const fetch = require('node-fetch');
const url = 'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true';
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
try {
const response = await fetch(url, options);
const text = await response.text();
return text;
} catch (error) {
console.error(error);
return '';
}`
const ToolDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const { confirm } = useConfirm()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getSpecificToolApi = useApi(toolsApi.getSpecificTool)
const [toolId, setToolId] = useState('')
const [toolName, setToolName] = useState('')
const [toolDesc, setToolDesc] = useState('')
const [toolSchema, setToolSchema] = useState([])
const [toolFunc, setToolFunc] = useState('')
const deleteItem = useCallback(
(id) => () => {
setTimeout(() => {
setToolSchema((prevRows) => prevRows.filter((row) => row.id !== id))
})
},
[]
)
const addNewRow = () => {
setTimeout(() => {
setToolSchema((prevRows) => {
let allRows = [...cloneDeep(prevRows)]
const lastRowId = allRows.length ? allRows[allRows.length - 1].id + 1 : 1
allRows.push({
id: lastRowId,
property: '',
description: '',
type: '',
required: false
})
return allRows
})
})
}
const onRowUpdate = (newRow) => {
setTimeout(() => {
setToolSchema((prevRows) => {
let allRows = [...cloneDeep(prevRows)]
const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id)
if (indexToUpdate >= 0) {
allRows[indexToUpdate] = { ...newRow }
}
return allRows
})
})
}
const columns = useMemo(
() => [
{ field: 'property', headerName: 'Property', editable: true, flex: 1 },
{
field: 'type',
headerName: 'Type',
type: 'singleSelect',
valueOptions: ['string', 'number', 'boolean', 'date'],
editable: true,
width: 120
},
{ field: 'description', headerName: 'Description', editable: true, flex: 1 },
{ field: 'required', headerName: 'Required', type: 'boolean', editable: true, width: 80 },
{
field: 'actions',
type: 'actions',
width: 80,
getActions: (params) => [
<GridActionsCellItem key={'Delete'} icon={<DeleteIcon />} label='Delete' onClick={deleteItem(params.id)} />
]
}
],
[deleteItem]
)
const formatSchema = (schema) => {
try {
const parsedSchema = JSON.parse(schema)
return parsedSchema.map((sch, index) => {
return {
...sch,
id: index
}
})
} catch (e) {
return []
}
}
useEffect(() => {
if (getSpecificToolApi.data) {
setToolId(getSpecificToolApi.data.id)
setToolName(getSpecificToolApi.data.name)
setToolDesc(getSpecificToolApi.data.description)
setToolSchema(formatSchema(getSpecificToolApi.data.schema))
if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func)
else setToolFunc('')
}
}, [getSpecificToolApi.data])
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
setToolId(dialogProps.data.id)
setToolName(dialogProps.data.name)
setToolDesc(dialogProps.data.description)
setToolSchema(formatSchema(dialogProps.data.schema))
if (dialogProps.data.func) setToolFunc(dialogProps.data.func)
else setToolFunc('')
} else if (dialogProps.type === 'EDIT' && dialogProps.toolId) {
getSpecificToolApi.request(dialogProps.toolId)
} else if (dialogProps.type === 'ADD') {
setToolId('')
setToolName('')
setToolDesc('')
setToolSchema([])
setToolFunc('')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
const addNewTool = async () => {
try {
const obj = {
name: toolName,
description: toolDesc,
color: generateRandomGradient(),
schema: JSON.stringify(toolSchema),
func: toolFunc
}
const createResp = await toolsApi.createNewTool(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Tool added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to add new Tool: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const saveTool = async () => {
try {
const saveResp = await toolsApi.updateTool(toolId, {
name: toolName,
description: toolDesc,
schema: JSON.stringify(toolSchema),
func: toolFunc
})
if (saveResp.data) {
enqueueSnackbar({
message: 'Tool saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
console.error(error)
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to save Tool: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const deleteTool = async () => {
const confirmPayload = {
title: `Delete Tool`,
description: `Delete tool ${toolName}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const delResp = await toolsApi.deleteTool(toolId)
if (delResp.data) {
enqueueSnackbar({
message: 'Tool deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm()
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to delete Tool: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='md'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Tool Name
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='toolName'
type='string'
fullWidth
placeholder='My New Tool'
value={toolName}
name='toolName'
onChange={(e) => setToolName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Tool description
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='toolDesc'
type='string'
fullWidth
placeholder='Description of what the tool does. This is for ChatGPT to determine when to use this tool.'
multiline={true}
rows={3}
value={toolDesc}
name='toolDesc'
onChange={(e) => setToolDesc(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Output Schema
<TooltipWithParser style={{ marginLeft: 10 }} title={'What should be the output response in JSON format?'} />
</Typography>
</Stack>
<Grid columns={columns} rows={toolSchema} addNewRow={addNewRow} onRowUpdate={onRowUpdate} />
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Javascript Function
<TooltipWithParser
style={{ marginLeft: 10 }}
title='Function to execute when tool is being used. You can use properties specified in Output Schema as variables. For example, if the property is <code>userid</code>, you can use as <code>$userid</code>. Return value must be a string.'
/>
</Typography>
</Stack>
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
See Example
</Button>
{customization.isDarkMode ? (
<DarkCodeEditor
value={toolFunc}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
borderRadius: 5
}}
/>
) : (
<LightCodeEditor
value={toolFunc}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
border: `1px solid ${theme.palette.grey[300]}`,
borderRadius: 5
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>
{dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteTool()}>
Delete
</StyledButton>
)}
<StyledButton
disabled={!(toolName && toolDesc)}
variant='contained'
onClick={() => (dialogProps.type === 'ADD' ? addNewTool() : saveTool())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
ToolDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default ToolDialog
+112
View File
@@ -0,0 +1,112 @@
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { Grid, Box, Stack } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import ItemCard from 'ui-component/cards/ItemCard'
import { gridSpacing } from 'store/constant'
import ToolEmptySVG from 'assets/images/tools_empty.svg'
import { StyledButton } from 'ui-component/button/StyledButton'
import ToolDialog from './ToolDialog'
// API
import toolsApi from 'api/tools'
// Hooks
import useApi from 'hooks/useApi'
// icons
import { IconPlus } from '@tabler/icons'
// ==============================|| CHATFLOWS ||============================== //
const Tools = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const getAllToolsApi = useApi(toolsApi.getAllTools)
const [showDialog, setShowDialog] = useState(false)
const [dialogProps, setDialogProps] = useState({})
const addNew = () => {
const dialogProp = {
title: 'Add New Tool',
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add'
}
setDialogProps(dialogProp)
setShowDialog(true)
}
const edit = (selectedTool) => {
const dialogProp = {
title: 'Edit Tool',
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: selectedTool
}
setDialogProps(dialogProp)
setShowDialog(true)
}
const onConfirm = () => {
setShowDialog(false)
getAllToolsApi.request()
}
useEffect(() => {
getAllToolsApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Tools</h1>
<Grid sx={{ mb: 1.25 }} container direction='row'>
<Box sx={{ flexGrow: 1 }} />
<Grid item>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
Create New
</StyledButton>
</Grid>
</Grid>
</Stack>
<Grid container spacing={gridSpacing}>
{!getAllToolsApi.loading &&
getAllToolsApi.data &&
getAllToolsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard data={data} color={data.color} onClick={() => edit(data)} />
</Grid>
))}
</Grid>
{!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' />
</Box>
<div>No Tools Created Yet</div>
</Stack>
)}
</MainCard>
<ToolDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={() => setShowDialog(false)}
onConfirm={onConfirm}
></ToolDialog>
</>
)
}
export default Tools