mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 17:01:00 +03:00
Feature/seq agents (#2798)
* update build functions * sequential agents * update langchain to 0.2, added sequential agent nodes * add marketplace templates * update howto wordings * Merge branch 'main' into feature/Seq-Agents # Conflicts: # pnpm-lock.yaml * update deprecated functions and add new sequential nodes * add marketplace templates * update marketplace templates, add structured output to llm node * add multi agents template * update llm node with bindmodels * update cypress version * update templates sticky note wordings * update tool node to include human in loop action * update structured outputs error from models * update cohere package to resolve google genai pipeThrough bug * update mistral package version, added message reconstruction before invoke seq agent * add HITL to agent * update state messages restructuring * update load and split methods for s3 directory
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,95 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// MUI
|
||||
import { Button, Dialog, DialogActions, DialogContent } from '@mui/material'
|
||||
import { Tabs } from '@mui/base/Tabs'
|
||||
|
||||
// Project Import
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { TabPanel } from '@/ui-component/tabs/TabPanel'
|
||||
import { TabsList } from '@/ui-component/tabs/TabsList'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||
|
||||
// Store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
|
||||
const ConditionDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
const [tabValue, setTabValue] = useState(0)
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.inputParam) {
|
||||
setInputParam(dialogProps.inputParam)
|
||||
}
|
||||
|
||||
if (dialogProps.data) setData(dialogProps.data)
|
||||
|
||||
return () => {
|
||||
setInputParam(null)
|
||||
setData({})
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogContent>
|
||||
<>
|
||||
{inputParam && inputParam.type.includes('conditionFunction') && (
|
||||
<>
|
||||
<Tabs value={tabValue} onChange={(event, val) => setTabValue(val)} aria-label='tabs' variant='fullWidth'>
|
||||
<TabsList>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<Tab key={index}>{inputChildParam.label}</Tab>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={tabValue} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(data, inputParam, tabValue)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ConditionDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default ConditionDialog
|
||||
@@ -22,7 +22,7 @@ import useApi from '@/hooks/useApi'
|
||||
|
||||
import './ExpandTextDialog.css'
|
||||
|
||||
const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicked, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const theme = useTheme()
|
||||
@@ -38,13 +38,18 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) setInputValue(dialogProps.value)
|
||||
if (dialogProps.value) {
|
||||
setInputValue(dialogProps.value)
|
||||
}
|
||||
if (dialogProps.inputParam) {
|
||||
setInputParam(dialogProps.inputParam)
|
||||
if (dialogProps.inputParam.type === 'code') {
|
||||
setLanguageType('js')
|
||||
}
|
||||
}
|
||||
if (dialogProps.languageType) {
|
||||
setLanguageType(dialogProps.languageType)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
@@ -91,9 +96,22 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
|
||||
<div style={{ flex: 70 }}>
|
||||
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
|
||||
{inputParam.label}
|
||||
</Typography>
|
||||
<div style={{ marginBottom: '10px', display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography variant='h4'>{inputParam.label}</Typography>
|
||||
<div style={{ flex: 1 }} />
|
||||
{inputParam.hint && (
|
||||
<Button
|
||||
sx={{ p: 0, px: 2 }}
|
||||
color='secondary'
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
onInputHintDialogClicked(inputParam.hint)
|
||||
}}
|
||||
>
|
||||
{inputParam.hint.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
border: '1px solid',
|
||||
@@ -114,7 +132,12 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
placeholder={inputParam.placeholder}
|
||||
basicSetup={
|
||||
languageType !== 'js'
|
||||
? { lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }
|
||||
? {
|
||||
lineNumbers: false,
|
||||
foldGutter: false,
|
||||
autocompletion: false,
|
||||
highlightActiveLine: false
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
@@ -123,7 +146,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{languageType === 'js' && (
|
||||
{languageType === 'js' && !inputParam.hideCodeExecute && (
|
||||
<LoadingButton
|
||||
sx={{
|
||||
mt: 2,
|
||||
@@ -177,7 +200,8 @@ ExpandTextDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
onConfirm: PropTypes.func,
|
||||
onInputHintDialogClicked: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExpandTextDialog
|
||||
|
||||
@@ -46,6 +46,7 @@ const FormatPromptValuesDialog = ({ show, dialogProps, onChange, onCancel }) =>
|
||||
nodes={dialogProps.nodes}
|
||||
edges={dialogProps.edges}
|
||||
nodeId={dialogProps.nodeId}
|
||||
isSequentialAgent={dialogProps.data.category === 'Sequential Agents'}
|
||||
/>
|
||||
</PerfectScrollbar>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
|
||||
const InputHintDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
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.label}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax, rehypeRaw]}
|
||||
components={{
|
||||
code({ inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dialogProps?.value}
|
||||
</MemoizedReactMarkdown>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
InputHintDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default InputHintDialog
|
||||
@@ -34,7 +34,7 @@ import userPNG from '@/assets/images/account.png'
|
||||
import msgEmptySVG from '@/assets/images/message_empty.svg'
|
||||
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
|
||||
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
|
||||
import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons-react'
|
||||
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons-react'
|
||||
|
||||
// Project import
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
@@ -846,6 +846,64 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
</Box>
|
||||
<div>{agent.agentName}</div>
|
||||
</Stack>
|
||||
{agent.usedTools && agent.usedTools.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{agent.usedTools.map((tool, index) => {
|
||||
return tool !== null ? (
|
||||
<Chip
|
||||
size='small'
|
||||
key={index}
|
||||
label={tool.tool}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconTool size={15} />}
|
||||
onClick={() =>
|
||||
onSourceDialogClick(
|
||||
tool,
|
||||
'Used Tools'
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{agent.state &&
|
||||
Object.keys(agent.state).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
size='small'
|
||||
label={'State'}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={
|
||||
<IconDeviceSdCard size={15} />
|
||||
}
|
||||
onClick={() =>
|
||||
onSourceDialogClick(
|
||||
agent.state,
|
||||
'State'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{agent.messages.length > 0 && (
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
@@ -893,6 +951,67 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
{agent.instructions && <p>{agent.instructions}</p>}
|
||||
{agent.messages.length === 0 &&
|
||||
!agent.instructions && <p>Finished</p>}
|
||||
{agent.sourceDocuments &&
|
||||
agent.sourceDocuments.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{removeDuplicateURL(agent).map(
|
||||
(source, index) => {
|
||||
const URL =
|
||||
source &&
|
||||
source.metadata &&
|
||||
source.metadata.source
|
||||
? isValidURL(
|
||||
source.metadata
|
||||
.source
|
||||
)
|
||||
: undefined
|
||||
return (
|
||||
<Chip
|
||||
size='small'
|
||||
key={index}
|
||||
label={
|
||||
URL
|
||||
? URL.pathname.substring(
|
||||
0,
|
||||
15
|
||||
) === '/'
|
||||
? URL.host
|
||||
: `${URL.pathname.substring(
|
||||
0,
|
||||
15
|
||||
)}...`
|
||||
: `${source.pageContent.substring(
|
||||
0,
|
||||
15
|
||||
)}...`
|
||||
}
|
||||
component='a'
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
onClick={() =>
|
||||
URL
|
||||
? onURLClick(
|
||||
source
|
||||
.metadata
|
||||
.source
|
||||
)
|
||||
: onSourceDialogClick(
|
||||
source
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,32 @@ import { Button } from '@mui/material'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { formatDataGridRows } from '@/utils/genericHelper'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
const StyledDataGrid = styled(MUIDataGrid)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.mode === 'light' ? '#b4b4b4' : '#303030'}`,
|
||||
|
||||
letterSpacing: 'normal',
|
||||
'& .MuiDataGrid-columnsContainer': {
|
||||
backgroundColor: theme.palette.mode === 'light' ? '#fafafa' : '#1d1d1d'
|
||||
},
|
||||
'& .MuiDataGrid-iconSeparator': {
|
||||
display: 'none'
|
||||
},
|
||||
'& .MuiDataGrid-columnHeader, .MuiDataGrid-cell': {
|
||||
borderRight: `1px solid ${theme.palette.mode === 'light' ? '#f0f0f0' : '#303030'}`
|
||||
},
|
||||
'& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell': {
|
||||
borderBottom: `1px solid ${theme.palette.mode === 'light' ? '#f0f0f0' : '#303030'}`
|
||||
},
|
||||
|
||||
'& .MuiPaginationItem-root': {
|
||||
borderRadius: 0
|
||||
},
|
||||
'& .MuiDataGrid-columnHeader:last-child, .MuiDataGrid-cell:last-child': {
|
||||
borderRight: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
export const DataGrid = ({ columns, rows, style, disabled = false, hideFooter = false, onChange }) => {
|
||||
const [rowValues, setRowValues] = useState(formatDataGridRows(rows) ?? [])
|
||||
@@ -80,7 +106,7 @@ export const DataGrid = ({ columns, rows, style, disabled = false, hideFooter =
|
||||
<>
|
||||
{rowValues && colValues && (
|
||||
<div style={{ marginTop: 10, height: 210, width: '100%', ...style }}>
|
||||
<MUIDataGrid
|
||||
<StyledDataGrid
|
||||
processRowUpdate={handleProcessRowUpdate}
|
||||
isCellEditable={() => {
|
||||
return !disabled
|
||||
|
||||
@@ -6,7 +6,17 @@ import SelectVariable from './SelectVariable'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
export const JsonEditorInput = ({ value, onChange, inputParam, nodes, edges, nodeId, disabled = false, isDarkMode = false }) => {
|
||||
export const JsonEditorInput = ({
|
||||
value,
|
||||
onChange,
|
||||
inputParam,
|
||||
nodes,
|
||||
edges,
|
||||
nodeId,
|
||||
disabled = false,
|
||||
isDarkMode = false,
|
||||
isSequentialAgent = false
|
||||
}) => {
|
||||
const [myValue, setMyValue] = useState(value ? JSON.parse(value) : {})
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
const [mouseUpKey, setMouseUpKey] = useState('')
|
||||
@@ -110,6 +120,7 @@ export const JsonEditorInput = ({ value, onChange, inputParam, nodes, edges, nod
|
||||
setNewVal(val)
|
||||
handleClosePopOver()
|
||||
}}
|
||||
isSequentialAgent={isSequentialAgent}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
@@ -125,5 +136,6 @@ JsonEditorInput.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
nodes: PropTypes.array,
|
||||
edges: PropTypes.array,
|
||||
nodeId: PropTypes.string
|
||||
nodeId: PropTypes.string,
|
||||
isSequentialAgent: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -4,9 +4,29 @@ import { Box, List, ListItemButton, ListItem, ListItemAvatar, ListItemText, Typo
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import robotPNG from '@/assets/images/robot.png'
|
||||
import chatPNG from '@/assets/images/chathistory.png'
|
||||
import diskPNG from '@/assets/images/floppy-disc.png'
|
||||
import { baseURL } from '@/store/constant'
|
||||
|
||||
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal }) => {
|
||||
const sequentialStateMessagesSelection = [
|
||||
{
|
||||
primary: '$flow.state.messages',
|
||||
secondary: `All messages from the start of the conversation till now`
|
||||
},
|
||||
{
|
||||
primary: '$flow.state.<replace-with-key>',
|
||||
secondary: `Current value of the state variable with specified key`
|
||||
},
|
||||
{
|
||||
primary: '$flow.state.messages[0].content',
|
||||
secondary: `First message content`
|
||||
},
|
||||
{
|
||||
primary: '$flow.state.messages[-1].content',
|
||||
secondary: `Last message content`
|
||||
}
|
||||
]
|
||||
|
||||
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal, isSequentialAgent }) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const onSelectOutputResponseClick = (node, prefix) => {
|
||||
@@ -102,9 +122,10 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
|
||||
{availableNodesForVariable &&
|
||||
availableNodesForVariable.length > 0 &&
|
||||
availableNodesForVariable.map((node, index) => {
|
||||
const selectedOutputAnchor = node.data.outputAnchors[0].options.find(
|
||||
(ancr) => ancr.name === node.data.outputs['output']
|
||||
)
|
||||
const selectedOutputAnchor =
|
||||
node.data.outputAnchors.length &&
|
||||
node.data.outputAnchors[0].options &&
|
||||
node.data.outputAnchors[0].options.find((ancr) => ancr.name === node.data.outputs['output'])
|
||||
return (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
@@ -157,6 +178,45 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
{isSequentialAgent &&
|
||||
(sequentialStateMessagesSelection || []).map((item, index) => (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelectAndReturnVal(item.primary)}
|
||||
>
|
||||
<ListItem alignItems='center'>
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 10,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt='state'
|
||||
src={diskPNG}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText sx={{ ml: 1 }} primary={item.primary} secondary={item.secondary} />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
@@ -169,7 +229,8 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
|
||||
SelectVariable.propTypes = {
|
||||
availableNodesForVariable: PropTypes.array,
|
||||
disabled: PropTypes.bool,
|
||||
onSelectAndReturnVal: PropTypes.func
|
||||
onSelectAndReturnVal: PropTypes.func,
|
||||
isSequentialAgent: PropTypes.bool
|
||||
}
|
||||
|
||||
export default SelectVariable
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.react-markdown table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
border-color: inherit !important;
|
||||
display: block !important;
|
||||
width: max-content !important;
|
||||
max-width: 100% !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.react-markdown tbody,
|
||||
.react-markdown td,
|
||||
.react-markdown tfoot,
|
||||
.react-markdown th,
|
||||
.react-markdown thead,
|
||||
.react-markdown tr {
|
||||
border-color: inherit !important;
|
||||
border-style: solid !important;
|
||||
border-width: 1px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
@@ -1,4 +1,19 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import './Markdown.css'
|
||||
|
||||
export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children)
|
||||
export const MemoizedReactMarkdown = memo(
|
||||
({ children, ...props }) => (
|
||||
<div className='react-markdown'>
|
||||
<ReactMarkdown {...props}>{children}</ReactMarkdown>
|
||||
</div>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.children === nextProps.children
|
||||
)
|
||||
|
||||
MemoizedReactMarkdown.displayName = 'MemoizedReactMarkdown'
|
||||
|
||||
MemoizedReactMarkdown.propTypes = {
|
||||
children: PropTypes.any
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { styled } from '@mui/system'
|
||||
import { buttonClasses } from '@mui/base/Button'
|
||||
import { Tab as BaseTab, tabClasses } from '@mui/base/Tab'
|
||||
import { blue } from './tabColors'
|
||||
|
||||
export const Tab = styled(BaseTab)(
|
||||
({ ...props }) => `
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
padding: 8px 12px;
|
||||
margin: 6px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props.sx?.backgroundColor || blue[400]};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
outline: 3px solid ${props.sx?.backgroundColor || blue[200]};
|
||||
}
|
||||
|
||||
&.${tabClasses.selected} {
|
||||
background-color: #fff;
|
||||
color: ${blue[600]};
|
||||
}
|
||||
|
||||
&.${buttonClasses.disabled} {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
export const TabPanel = (props) => {
|
||||
const { children, value, index, ...other } = props
|
||||
return (
|
||||
<div role='tabpanel' hidden={value !== index} id={`tabpanel-${index}`} aria-labelledby={`tab-${index}`} {...other}>
|
||||
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { styled } from '@mui/system'
|
||||
import { TabsList as BaseTabsList } from '@mui/base/TabsList'
|
||||
import { blue } from './tabColors'
|
||||
|
||||
export const TabsList = styled(BaseTabsList)(
|
||||
({ theme, ...props }) => `
|
||||
min-width: 400px;
|
||||
background-color: ${props.sx?.backgroundColor || blue[500]};
|
||||
border-radius: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: space-between;
|
||||
box-shadow: 0px 4px 6px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.4)' : 'rgba(0,0,0, 0.2)'};
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
export const blue = {
|
||||
50: '#F0F7FF',
|
||||
100: '#C2E0FF',
|
||||
200: '#80BFFF',
|
||||
300: '#66B2FF',
|
||||
400: '#3399FF',
|
||||
500: '#007FFF',
|
||||
600: '#0072E5',
|
||||
700: '#0059B2',
|
||||
800: '#004C99',
|
||||
900: '#003A75'
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import moment from 'moment'
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
// Get amount of same nodes
|
||||
@@ -52,7 +53,9 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
'code',
|
||||
'date',
|
||||
'file',
|
||||
'folder'
|
||||
'folder',
|
||||
'tabs',
|
||||
'conditionFunction' // This is a special type for condition functions
|
||||
]
|
||||
|
||||
// Inputs
|
||||
@@ -80,6 +83,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
// Outputs
|
||||
const outputAnchors = []
|
||||
for (let i = 0; i < outgoing; i += 1) {
|
||||
if (nodeData.hideOutput) continue
|
||||
if (nodeData.outputs && nodeData.outputs.length) {
|
||||
const options = []
|
||||
for (let j = 0; j < nodeData.outputs.length; j += 1) {
|
||||
@@ -100,7 +104,9 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
name: nodeData.outputs[j].name,
|
||||
label: nodeData.outputs[j].label,
|
||||
description: nodeData.outputs[j].description ?? '',
|
||||
type
|
||||
type,
|
||||
isAnchor: nodeData.outputs[j]?.isAnchor,
|
||||
hidden: nodeData.outputs[j]?.hidden
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
}
|
||||
@@ -404,14 +410,43 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string|json-llmChain_1-llmChain_1-input-promptValues-string"
|
||||
// {source} -{sourceHandle} -{target} -{targetHandle}
|
||||
const parentNodes = []
|
||||
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
|
||||
if (inputEdges && inputEdges.length) {
|
||||
for (let j = 0; j < inputEdges.length; j += 1) {
|
||||
const node = nodes.find((nd) => nd.id === inputEdges[j].source)
|
||||
parentNodes.push(node)
|
||||
}
|
||||
|
||||
const isSeqAgent = nodes.find((nd) => nd.id === target)?.data?.category === 'Sequential Agents'
|
||||
|
||||
function collectParentNodes(targetNodeId, nodes, edges) {
|
||||
const inputEdges = edges.filter(
|
||||
(edg) => edg.target === targetNodeId && edg.targetHandle.includes(`${targetNodeId}-input-sequentialNode`)
|
||||
)
|
||||
|
||||
// Traverse each edge found
|
||||
inputEdges.forEach((edge) => {
|
||||
const parentNode = nodes.find((nd) => nd.id === edge.source)
|
||||
if (!parentNode) return
|
||||
|
||||
// Recursive call to explore further up the tree
|
||||
collectParentNodes(parentNode.id, nodes, edges)
|
||||
|
||||
// Check and add the parent node to the list if it does not include specific names
|
||||
const excludeNodeNames = ['seqAgent', 'seqLLMNode', 'seqToolNode']
|
||||
if (excludeNodeNames.includes(parentNode.data.name)) {
|
||||
parentNodes.push(parentNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isSeqAgent) {
|
||||
collectParentNodes(target, nodes, edges)
|
||||
return uniq(parentNodes)
|
||||
} else {
|
||||
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
|
||||
if (inputEdges && inputEdges.length) {
|
||||
for (let j = 0; j < inputEdges.length; j += 1) {
|
||||
const node = nodes.find((nd) => nd.id === inputEdges[j].source)
|
||||
parentNodes.push(node)
|
||||
}
|
||||
}
|
||||
return parentNodes
|
||||
}
|
||||
return parentNodes
|
||||
}
|
||||
|
||||
export const getUpsertDetails = (nodes, edges) => {
|
||||
@@ -783,3 +818,115 @@ export const kFormatter = (num) => {
|
||||
const item = lookup.findLast((item) => num >= item.value)
|
||||
return item ? (num / item.value).toFixed(1).replace(regexp, '').concat(item.symbol) : '0'
|
||||
}
|
||||
|
||||
const toCamelCase = (str) => {
|
||||
return str
|
||||
.split(' ') // Split by space to process each word
|
||||
.map((word, index) => (index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()))
|
||||
.join('') // Join the words back into a single string
|
||||
}
|
||||
|
||||
const createJsonArray = (labels) => {
|
||||
return labels.map((label) => {
|
||||
return {
|
||||
label: label,
|
||||
name: toCamelCase(label),
|
||||
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
|
||||
isAnchor: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getCustomConditionOutputs = (value, nodeId, existingEdges, isDataGrid) => {
|
||||
// Regex to find return statements and capture returned values
|
||||
const regex = /return\s+(['"`])(.*?)\1/g
|
||||
let match
|
||||
const numberOfReturns = []
|
||||
|
||||
if (!isDataGrid) {
|
||||
// Loop over the matches of the regex
|
||||
while ((match = regex.exec(value)) !== null) {
|
||||
// Push the captured group, which is the actual return value, into results
|
||||
numberOfReturns.push(match[2])
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value)
|
||||
if (parsedValue && parsedValue.length) {
|
||||
for (const item of parsedValue) {
|
||||
if (!item.variable) {
|
||||
alert('Please specify a Variable. Try connecting Condition node to a previous node and select the variable')
|
||||
return undefined
|
||||
}
|
||||
if (!item.output) {
|
||||
alert('Please specify an Output Name')
|
||||
return undefined
|
||||
}
|
||||
if (!item.operation) {
|
||||
alert('Please select an operation for the condition')
|
||||
return undefined
|
||||
}
|
||||
numberOfReturns.push(item.output)
|
||||
}
|
||||
numberOfReturns.push('End')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (numberOfReturns.length === 0) {
|
||||
if (isDataGrid) alert('Please add an item for the condition')
|
||||
else
|
||||
alert(
|
||||
'Please add a return statement in the condition code to define the output. You can refer to How to Use for more information.'
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const outputs = createJsonArray(numberOfReturns.sort())
|
||||
|
||||
const outputAnchors = []
|
||||
|
||||
const options = []
|
||||
for (let j = 0; j < outputs.length; j += 1) {
|
||||
let baseClasses = ''
|
||||
let type = ''
|
||||
|
||||
const outputBaseClasses = outputs[j].baseClasses ?? []
|
||||
if (outputBaseClasses.length > 1) {
|
||||
baseClasses = outputBaseClasses.join('|')
|
||||
type = outputBaseClasses.join(' | ')
|
||||
} else if (outputBaseClasses.length === 1) {
|
||||
baseClasses = outputBaseClasses[0]
|
||||
type = outputBaseClasses[0]
|
||||
}
|
||||
|
||||
const newOutputOption = {
|
||||
id: `${nodeId}-output-${outputs[j].name}-${baseClasses}`,
|
||||
name: outputs[j].name,
|
||||
label: outputs[j].label,
|
||||
type,
|
||||
isAnchor: outputs[j]?.isAnchor
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
}
|
||||
const newOutput = {
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
options
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
|
||||
// Remove edges
|
||||
let newEdgeSourceHandles = []
|
||||
for (const anchor of options) {
|
||||
const anchorId = anchor.id
|
||||
newEdgeSourceHandles.push(anchorId)
|
||||
}
|
||||
|
||||
const toBeRemovedEdgeIds = existingEdges.filter((edge) => !newEdgeSourceHandles.includes(edge.sourceHandle)).map((edge) => edge.id)
|
||||
|
||||
return { outputAnchors, toBeRemovedEdgeIds }
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ function a11yProps(index) {
|
||||
|
||||
const blacklistCategoriesForAgentCanvas = ['Agents', 'Memory', 'Record Manager']
|
||||
const allowedAgentModel = {}
|
||||
const exceptions = {
|
||||
Memory: ['agentMemory']
|
||||
}
|
||||
|
||||
const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
const theme = useTheme()
|
||||
@@ -84,9 +87,19 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
filterSearch(searchValue, newValue)
|
||||
}
|
||||
|
||||
const addException = () => {
|
||||
let nodes = []
|
||||
for (const category in exceptions) {
|
||||
const nodeNames = exceptions[category]
|
||||
nodes.push(...nodesData.filter((nd) => nd.category === category && nodeNames.includes(nd.name)))
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
const getSearchedNodes = (value) => {
|
||||
if (isAgentCanvas) {
|
||||
const nodes = nodesData.filter((nd) => !blacklistCategoriesForAgentCanvas.includes(nd.category))
|
||||
nodes.push(...addException())
|
||||
const passed = nodes.filter((nd) => {
|
||||
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
|
||||
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
|
||||
@@ -94,7 +107,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
})
|
||||
return passed
|
||||
}
|
||||
const nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents')
|
||||
const nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents' && nd.category !== 'Sequential Agents')
|
||||
const passed = nodes.filter((nd) => {
|
||||
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
|
||||
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
|
||||
@@ -156,9 +169,15 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
filteredResult[category] = nodes
|
||||
}
|
||||
}
|
||||
|
||||
// Allow exceptions
|
||||
if (Object.keys(exceptions).includes(category)) {
|
||||
filteredResult[category] = addException()
|
||||
}
|
||||
}
|
||||
setNodes(filteredResult)
|
||||
accordianCategories['Multi Agents'] = true
|
||||
accordianCategories['Sequential Agents'] = true
|
||||
setCategoryExpanded(accordianCategories)
|
||||
} else {
|
||||
const taggedNodes = groupByTags(nodes, newTabValue)
|
||||
@@ -172,7 +191,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
|
||||
const filteredResult = {}
|
||||
for (const category in result) {
|
||||
if (category === 'Multi Agents') {
|
||||
if (category === 'Multi Agents' || category === 'Sequential Agents') {
|
||||
continue
|
||||
}
|
||||
filteredResult[category] = result[category]
|
||||
|
||||
@@ -34,6 +34,7 @@ const CanvasNode = ({ data }) => {
|
||||
const [infoDialogProps, setInfoDialogProps] = useState({})
|
||||
const [warningMessage, setWarningMessage] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isForceCloseNodeInfo, setIsForceCloseNodeInfo] = useState(null)
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
@@ -43,6 +44,11 @@ const CanvasNode = ({ data }) => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const getNodeInfoOpenStatus = () => {
|
||||
if (isForceCloseNodeInfo) return false
|
||||
else return !canvas.canvasDialogShow && open
|
||||
}
|
||||
|
||||
const nodeOutdatedMessage = (oldVersion, newVersion) => `Node version ${oldVersion} outdated\nUpdate to latest version ${newVersion}`
|
||||
|
||||
const nodeVersionEmptyMessage = (newVersion) => `Node outdated\nUpdate to latest version ${newVersion}`
|
||||
@@ -87,7 +93,7 @@ const CanvasNode = ({ data }) => {
|
||||
border={false}
|
||||
>
|
||||
<NodeTooltip
|
||||
open={!canvas.canvasDialogShow && open}
|
||||
open={getNodeInfoOpenStatus()}
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
disableFocusListener={true}
|
||||
@@ -213,7 +219,18 @@ const CanvasNode = ({ data }) => {
|
||||
{data.inputParams
|
||||
.filter((inputParam) => !inputParam.hidden)
|
||||
.map((inputParam, index) => (
|
||||
<NodeInputHandler key={index} inputParam={inputParam} data={data} />
|
||||
<NodeInputHandler
|
||||
key={index}
|
||||
inputParam={inputParam}
|
||||
data={data}
|
||||
onHideNodeInfoDialog={(status) => {
|
||||
if (status) {
|
||||
setIsForceCloseNodeInfo(true)
|
||||
} else {
|
||||
setIsForceCloseNodeInfo(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{data.inputParams.find((param) => param.additionalParams) && (
|
||||
<div
|
||||
@@ -231,21 +248,24 @@ const CanvasNode = ({ data }) => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
{data.outputAnchors.map((outputAnchor) => (
|
||||
<NodeOutputHandler key={JSON.stringify(data)} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
{data.outputAnchors.length > 0 && <Divider />}
|
||||
{data.outputAnchors.length > 0 && (
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{data.outputAnchors.length > 0 && <Divider />}
|
||||
{data.outputAnchors.length > 0 &&
|
||||
data.outputAnchors.map((outputAnchor) => (
|
||||
<NodeOutputHandler key={JSON.stringify(data)} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
</Box>
|
||||
</NodeTooltip>
|
||||
</NodeCardWrapper>
|
||||
|
||||
@@ -5,10 +5,13 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
|
||||
import { Popper, Box, Typography, Tooltip, IconButton, Button, TextField } from '@mui/material'
|
||||
import { useGridApiContext } from '@mui/x-data-grid'
|
||||
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons-react'
|
||||
import { IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb } from '@tabler/icons-react'
|
||||
import { Tabs } from '@mui/base/Tabs'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
|
||||
// project import
|
||||
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
|
||||
@@ -19,20 +22,24 @@ import { DataGrid } from '@/ui-component/grid/DataGrid'
|
||||
import { File } from '@/ui-component/file/File'
|
||||
import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { isValidConnection } from '@/utils/genericHelper'
|
||||
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
import { TabPanel } from '@/ui-component/tabs/TabPanel'
|
||||
import { TabsList } from '@/ui-component/tabs/TabsList'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import ToolDialog from '@/views/tools/ToolDialog'
|
||||
import AssistantDialog from '@/views/assistants/AssistantDialog'
|
||||
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
|
||||
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
|
||||
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
|
||||
import CredentialInputHandler from './CredentialInputHandler'
|
||||
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
|
||||
|
||||
// utils
|
||||
import { getInputVariables } from '@/utils/genericHelper'
|
||||
import { getInputVariables, getCustomConditionOutputs, isValidConnection, getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
@@ -45,13 +52,33 @@ const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...prop
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ===========================|| NodeInputHandler ||=========================== //
|
||||
|
||||
const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isAdditionalParams = false }) => {
|
||||
const NodeInputHandler = ({
|
||||
inputAnchor,
|
||||
inputParam,
|
||||
data,
|
||||
disabled = false,
|
||||
isAdditionalParams = false,
|
||||
disablePadding = false,
|
||||
onHideNodeInfoDialog
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
const { reactFlowInstance, deleteEdge } = useContext(flowContext)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
@@ -64,12 +91,26 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
const [showPromptHubDialog, setShowPromptHubDialog] = useState(false)
|
||||
const [showManageScrapedLinksDialog, setShowManageScrapedLinksDialog] = useState(false)
|
||||
const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({})
|
||||
const [showInputHintDialog, setShowInputHintDialog] = useState(false)
|
||||
const [inputHintDialogProps, setInputHintDialogProps] = useState({})
|
||||
const [showConditionDialog, setShowConditionDialog] = useState(false)
|
||||
const [conditionDialogProps, setConditionDialogProps] = useState({})
|
||||
const [tabValue, setTabValue] = useState(0)
|
||||
|
||||
const onExpandDialogClicked = (value, inputParam) => {
|
||||
const onInputHintDialogClicked = (hint) => {
|
||||
const dialogProps = {
|
||||
...hint
|
||||
}
|
||||
setInputHintDialogProps(dialogProps)
|
||||
setShowInputHintDialog(true)
|
||||
}
|
||||
|
||||
const onExpandDialogClicked = (value, inputParam, languageType) => {
|
||||
const dialogProps = {
|
||||
value,
|
||||
inputParam,
|
||||
disabled,
|
||||
languageType,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
@@ -77,6 +118,19 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
|
||||
const onConditionDialogClicked = (inputParam) => {
|
||||
const dialogProps = {
|
||||
data,
|
||||
inputParam,
|
||||
disabled,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
setConditionDialogProps(dialogProps)
|
||||
setShowConditionDialog(true)
|
||||
onHideNodeInfoDialog(true)
|
||||
}
|
||||
|
||||
const onShowPromptHubButtonClicked = () => {
|
||||
setShowPromptHubDialog(true)
|
||||
}
|
||||
@@ -120,6 +174,157 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
return ''
|
||||
}
|
||||
|
||||
const getDataGridColDef = (columns, inputParam) => {
|
||||
const colDef = []
|
||||
for (const column of columns) {
|
||||
const stateNode = reactFlowInstance ? reactFlowInstance.getNodes().find((node) => node.data.name === 'seqState') : null
|
||||
if (column.type === 'asyncSingleSelect' && column.loadMethod && column.loadMethod.includes('loadStateKeys')) {
|
||||
if (stateNode) {
|
||||
const tabParam = stateNode.data.inputParams.find((param) => param.tabIdentifier)
|
||||
if (tabParam && tabParam.tabs.length > 0) {
|
||||
const selectedTabIdentifier = tabParam.tabIdentifier
|
||||
|
||||
const selectedTab =
|
||||
stateNode.data.inputs[`${selectedTabIdentifier}_${stateNode.data.id}`] ||
|
||||
tabParam.default ||
|
||||
tabParam.tabs[0].name
|
||||
|
||||
const datagridValues = stateNode.data.inputs[selectedTab]
|
||||
if (datagridValues) {
|
||||
try {
|
||||
const parsedDatagridValues = JSON.parse(datagridValues)
|
||||
const keys = Array.isArray(parsedDatagridValues)
|
||||
? parsedDatagridValues.map((item) => item.key)
|
||||
: Object.keys(parsedDatagridValues)
|
||||
colDef.push({
|
||||
...column,
|
||||
field: column.field,
|
||||
headerName: column.headerName,
|
||||
type: 'singleSelect',
|
||||
editable: true,
|
||||
valueOptions: keys
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error parsing stateMemory', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
colDef.push({
|
||||
...column,
|
||||
field: column.field,
|
||||
headerName: column.headerName,
|
||||
type: 'singleSelect',
|
||||
editable: true,
|
||||
valueOptions: []
|
||||
})
|
||||
}
|
||||
} else if (column.type === 'freeSolo') {
|
||||
const preLoadOptions = []
|
||||
if (column.loadMethod && column.loadMethod.includes('getPreviousMessages')) {
|
||||
const nodes = getAvailableNodesForVariable(
|
||||
reactFlowInstance?.getNodes() || [],
|
||||
reactFlowInstance?.getEdges() || [],
|
||||
data.id,
|
||||
inputParam.id
|
||||
)
|
||||
for (const node of nodes) {
|
||||
preLoadOptions.push({
|
||||
value: `$${node.id}`,
|
||||
label: `Output from ${node.data.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
if (column.loadMethod && column.loadMethod.includes('loadStateKeys')) {
|
||||
if (stateNode) {
|
||||
const tabParam = stateNode.data.inputParams.find((param) => param.tabIdentifier)
|
||||
if (tabParam && tabParam.tabs.length > 0) {
|
||||
const selectedTabIdentifier = tabParam.tabIdentifier
|
||||
|
||||
const selectedTab =
|
||||
stateNode.data.inputs[`${selectedTabIdentifier}_${stateNode.data.id}`] ||
|
||||
tabParam.default ||
|
||||
tabParam.tabs[0].name
|
||||
|
||||
const datagridValues = stateNode.data.inputs[selectedTab]
|
||||
if (datagridValues) {
|
||||
try {
|
||||
const parsedDatagridValues = JSON.parse(datagridValues)
|
||||
const keys = Array.isArray(parsedDatagridValues)
|
||||
? parsedDatagridValues.map((item) => item.key)
|
||||
: Object.keys(parsedDatagridValues)
|
||||
for (const key of keys) {
|
||||
preLoadOptions.push({
|
||||
value: `$flow.state.${key}`,
|
||||
label: `Value from ${key}`
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing stateMemory', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
colDef.push({
|
||||
...column,
|
||||
field: column.field,
|
||||
headerName: column.headerName,
|
||||
renderEditCell: ({ id, field, value }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const apiRef = useGridApiContext()
|
||||
return (
|
||||
<Autocomplete
|
||||
id={column.field}
|
||||
freeSolo
|
||||
fullWidth
|
||||
options={[...preLoadOptions, ...column.valueOptions]}
|
||||
value={value}
|
||||
PopperComponent={StyledPopper}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props}>
|
||||
<div>
|
||||
<strong>{option.value}</strong>
|
||||
<br />
|
||||
<small>{option.label}</small>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={(option) => {
|
||||
return typeof option === 'string' ? option : option.value
|
||||
}}
|
||||
onInputChange={(event, newValue) => {
|
||||
apiRef.current.setEditCellValue({ id, field, value: newValue })
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
height: '50px' // Adjust this value as needed
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
border: 'none'
|
||||
},
|
||||
'& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
colDef.push(column)
|
||||
}
|
||||
}
|
||||
return colDef
|
||||
}
|
||||
|
||||
const getTabValue = (inputParam) => {
|
||||
return inputParam.tabs.findIndex((item) => item.name === data.inputs[`${inputParam.tabIdentifier}_${data.id}`]) >= 0
|
||||
? inputParam.tabs.findIndex((item) => item.name === data.inputs[`${inputParam.tabIdentifier}_${data.id}`])
|
||||
: tabValue
|
||||
}
|
||||
|
||||
const onEditJSONClicked = (value, inputParam) => {
|
||||
// Preset values if the field is format prompt values
|
||||
let inputValue = value
|
||||
@@ -134,17 +339,37 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
const dialogProp = {
|
||||
value: inputValue,
|
||||
inputParam,
|
||||
nodes: reactFlowInstance.getNodes(),
|
||||
edges: reactFlowInstance.getEdges(),
|
||||
nodeId: data.id
|
||||
nodes: reactFlowInstance?.getNodes() || [],
|
||||
edges: reactFlowInstance?.getEdges() || [],
|
||||
nodeId: data.id,
|
||||
data
|
||||
}
|
||||
setFormatPromptValuesDialogProps(dialogProp)
|
||||
setShowFormatPromptValuesDialog(true)
|
||||
}
|
||||
|
||||
const onExpandDialogSave = (newValue, inputParamName) => {
|
||||
setShowExpandDialog(false)
|
||||
data.inputs[inputParamName] = newValue
|
||||
setShowExpandDialog(false)
|
||||
}
|
||||
|
||||
const onConditionDialogSave = (newData, inputParam, tabValue) => {
|
||||
data.inputs[`${inputParam.tabIdentifier}_${data.id}`] = inputParam.tabs[tabValue].name
|
||||
|
||||
const existingEdges = reactFlowInstance?.getEdges().filter((edge) => edge.source === data.id) || []
|
||||
const { outputAnchors, toBeRemovedEdgeIds } = getCustomConditionOutputs(
|
||||
newData.inputs[inputParam.tabs[tabValue].name],
|
||||
data.id,
|
||||
existingEdges,
|
||||
inputParam.tabs[tabValue].type === 'datagrid'
|
||||
)
|
||||
if (!outputAnchors) return
|
||||
data.outputAnchors = outputAnchors
|
||||
for (const edgeId of toBeRemovedEdgeIds) {
|
||||
deleteEdge(edgeId)
|
||||
}
|
||||
setShowConditionDialog(false)
|
||||
onHideNodeInfoDialog(false)
|
||||
}
|
||||
|
||||
const editAsyncOption = (inputParamName, inputValue) => {
|
||||
@@ -240,7 +465,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
|
||||
{((inputParam && !inputParam.additionalParams) || isAdditionalParams) && (
|
||||
<>
|
||||
{inputParam.acceptVariable && (
|
||||
{inputParam.acceptVariable && !isAdditionalParams && (
|
||||
<CustomWidthTooltip placement='left' title={inputParam.type}>
|
||||
<Handle
|
||||
type='target'
|
||||
@@ -257,7 +482,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
)}
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ p: disablePadding ? 0 : 2 }}>
|
||||
{(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') &&
|
||||
(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && (
|
||||
<>
|
||||
@@ -290,6 +515,19 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
{inputParam.hint && isAdditionalParams && (
|
||||
<Button
|
||||
sx={{ p: 0, px: 2 }}
|
||||
color='secondary'
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
onInputHintDialogClicked(inputParam.hint)
|
||||
}}
|
||||
startIcon={<IconBulb size={17} />}
|
||||
>
|
||||
{inputParam.hint.label}
|
||||
</Button>
|
||||
)}
|
||||
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
|
||||
<IconButton
|
||||
size='small'
|
||||
@@ -335,7 +573,37 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputParam.type === 'tabs' && (
|
||||
<>
|
||||
<Tabs
|
||||
value={getTabValue(inputParam)}
|
||||
onChange={(event, val) => {
|
||||
setTabValue(val)
|
||||
data.inputs[`${inputParam.tabIdentifier}_${data.id}`] = inputParam.tabs[val].name
|
||||
}}
|
||||
aria-label='tabs'
|
||||
variant='fullWidth'
|
||||
defaultValue={getTabValue(inputParam)}
|
||||
>
|
||||
<TabsList>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<Tab key={index}>{inputChildParam.label}</Tab>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={getTabValue(inputParam)} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{inputParam.type === 'file' && (
|
||||
<File
|
||||
disabled={disabled}
|
||||
@@ -354,7 +622,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
{inputParam.type === 'datagrid' && (
|
||||
<DataGrid
|
||||
disabled={disabled}
|
||||
columns={inputParam.datagrid}
|
||||
columns={getDataGridColDef(inputParam.datagrid, inputParam)}
|
||||
hideFooter={true}
|
||||
rows={data.inputs[inputParam.name] ?? JSON.stringify(inputParam.default) ?? []}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
@@ -362,8 +630,27 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
)}
|
||||
{inputParam.type === 'code' && (
|
||||
<>
|
||||
<div style={{ height: '5px' }}></div>
|
||||
<div style={{ height: inputParam.rows ? '100px' : '200px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
|
||||
{inputParam.codeExample && (
|
||||
<Button
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
data.inputs[inputParam.name] = inputParam.codeExample
|
||||
}}
|
||||
>
|
||||
See Example
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.grey['300'],
|
||||
borderRadius: '6px',
|
||||
height: inputParam.rows ? '100px' : '200px'
|
||||
}}
|
||||
>
|
||||
<CodeEditor
|
||||
disabled={disabled}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
@@ -401,6 +688,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
getJSONValue(data.inputs['workerPrompt']) ||
|
||||
''
|
||||
}
|
||||
isSequentialAgent={data.category === 'Sequential Agents'}
|
||||
isDarkMode={customization.isDarkMode}
|
||||
/>
|
||||
)}
|
||||
@@ -473,6 +761,23 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* CUSTOM INPUT LOGIC */}
|
||||
{inputParam.type.includes('conditionFunction') && (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
sx={{ borderRadius: '12px', width: '100%', mt: 1 }}
|
||||
variant='outlined'
|
||||
onClick={() => onConditionDialogClicked(inputParam)}
|
||||
>
|
||||
{inputParam.label}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(data.name === 'cheerioWebScraper' ||
|
||||
data.name === 'puppeteerWebScraper' ||
|
||||
data.name === 'playwrightWebScraper') &&
|
||||
@@ -526,7 +831,22 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
dialogProps={expandDialogProps}
|
||||
onCancel={() => setShowExpandDialog(false)}
|
||||
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
|
||||
onInputHintDialogClicked={onInputHintDialogClicked}
|
||||
></ExpandTextDialog>
|
||||
<ConditionDialog
|
||||
show={showConditionDialog}
|
||||
dialogProps={conditionDialogProps}
|
||||
onCancel={() => {
|
||||
setShowConditionDialog(false)
|
||||
onHideNodeInfoDialog(false)
|
||||
}}
|
||||
onConfirm={(newData, inputParam, tabValue) => onConditionDialogSave(newData, inputParam, tabValue)}
|
||||
></ConditionDialog>
|
||||
<InputHintDialog
|
||||
show={showInputHintDialog}
|
||||
dialogProps={inputHintDialogProps}
|
||||
onCancel={() => setShowInputHintDialog(false)}
|
||||
></InputHintDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -536,7 +856,9 @@ NodeInputHandler.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
isAdditionalParams: PropTypes.bool
|
||||
isAdditionalParams: PropTypes.bool,
|
||||
disablePadding: PropTypes.bool,
|
||||
onHideNodeInfoDialog: PropTypes.func
|
||||
}
|
||||
|
||||
export default NodeInputHandler
|
||||
|
||||
@@ -23,12 +23,29 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const [clientHeight, setClientHeight] = useState(0)
|
||||
const [offsetTop, setOffsetTop] = useState(0)
|
||||
const [dropdownValue, setDropdownValue] = useState(null)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
const getAvailableOptions = (options = []) => {
|
||||
return options.filter((option) => !option.hidden && !option.isAnchor)
|
||||
}
|
||||
|
||||
const getAnchorOptions = (options = []) => {
|
||||
return options.filter((option) => !option.hidden && option.isAnchor)
|
||||
}
|
||||
|
||||
const getAnchorPosition = (options, index) => {
|
||||
const spacing = clientHeight / (getAnchorOptions(options).length + 1)
|
||||
return offsetTop + spacing * (index + 1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current?.offsetTop && ref.current?.clientHeight) {
|
||||
setTimeout(() => {
|
||||
setClientHeight(ref.current?.clientHeight)
|
||||
setOffsetTop(ref.current?.offsetTop)
|
||||
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
|
||||
updateNodeInternals(data.id)
|
||||
}, 0)
|
||||
@@ -73,6 +90,38 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{data.name !== 'ifElseFunction' &&
|
||||
outputAnchor.type === 'options' &&
|
||||
outputAnchor.options &&
|
||||
getAnchorOptions(outputAnchor.options).length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{getAnchorOptions(outputAnchor.options).map((option, index) => {
|
||||
return (
|
||||
<div key={option.id} style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<CustomWidthTooltip placement='right' title={option.type}>
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={index}
|
||||
id={option?.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: getAnchorPosition(outputAnchor.options, index)
|
||||
}}
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<Box sx={{ p: 2, textAlign: 'end' }}>
|
||||
<Typography>{option.label}</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{data.name === 'ifElseFunction' && outputAnchor.type === 'options' && outputAnchor.options && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
@@ -134,7 +183,7 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
|
||||
{data.name !== 'ifElseFunction' &&
|
||||
outputAnchor.type === 'options' &&
|
||||
outputAnchor.options &&
|
||||
outputAnchor.options.length > 0 && (
|
||||
getAvailableOptions(outputAnchor.options).length > 0 && (
|
||||
<>
|
||||
<CustomWidthTooltip
|
||||
placement='right'
|
||||
@@ -161,7 +210,7 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
|
||||
disabled={disabled}
|
||||
disableClearable={true}
|
||||
name={outputAnchor.name}
|
||||
options={outputAnchor.options}
|
||||
options={getAvailableOptions(outputAnchor.options)}
|
||||
onSelect={(newValue) => {
|
||||
setDropdownValue(newValue)
|
||||
data.outputs[outputAnchor.name] = newValue
|
||||
|
||||
@@ -35,7 +35,9 @@ import {
|
||||
IconTrash,
|
||||
IconX,
|
||||
IconTool,
|
||||
IconSquareFilled
|
||||
IconSquareFilled,
|
||||
IconDeviceSdCard,
|
||||
IconCheck
|
||||
} from '@tabler/icons-react'
|
||||
import robotPNG from '@/assets/images/robot.png'
|
||||
import userPNG from '@/assets/images/account.png'
|
||||
@@ -54,9 +56,9 @@ import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-componen
|
||||
import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'
|
||||
import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'
|
||||
import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'
|
||||
import './ChatMessage.css'
|
||||
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
|
||||
import './audio-recording.css'
|
||||
import './ChatMessage.css'
|
||||
|
||||
// api
|
||||
import chatmessageApi from '@/api/chatmessage'
|
||||
@@ -408,7 +410,16 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].agentReasoning = JSON.parse(agentReasoning)
|
||||
allMessages[allMessages.length - 1].agentReasoning = agentReasoning
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageAction = (action) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].action = action
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
@@ -488,11 +499,22 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
handleSubmit(undefined, promptStarterInput)
|
||||
}
|
||||
|
||||
const handleActionClick = async (elem, action) => {
|
||||
setUserInput(elem.label)
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].action = null
|
||||
return allMessages
|
||||
})
|
||||
handleSubmit(undefined, elem.label, action)
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (e, promptStarterInput) => {
|
||||
const handleSubmit = async (e, selectedInput, action) => {
|
||||
if (e) e.preventDefault()
|
||||
|
||||
if (!promptStarterInput && userInput.trim() === '') {
|
||||
if (!selectedInput && userInput.trim() === '') {
|
||||
const containsAudio = previews.filter((item) => item.type === 'audio').length > 0
|
||||
if (!(previews.length >= 1 && containsAudio)) {
|
||||
return
|
||||
@@ -501,7 +523,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
|
||||
let input = userInput
|
||||
|
||||
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
|
||||
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
|
||||
|
||||
setLoading(true)
|
||||
const urls = previews.map((item) => {
|
||||
@@ -524,6 +546,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
if (urls && urls.length > 0) params.uploads = urls
|
||||
if (leadEmail) params.leadEmail = leadEmail
|
||||
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
|
||||
if (action) params.action = action
|
||||
|
||||
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
|
||||
|
||||
@@ -566,6 +589,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
usedTools: data?.usedTools,
|
||||
fileAnnotations: data?.fileAnnotations,
|
||||
agentReasoning: data?.agentReasoning,
|
||||
action: data?.action,
|
||||
type: 'apiMessage',
|
||||
feedback: null
|
||||
}
|
||||
@@ -638,6 +662,16 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
}
|
||||
}
|
||||
|
||||
const getAgentIcon = (nodeName, instructions) => {
|
||||
if (nodeName) {
|
||||
return `${baseURL}/api/v1/node-icon/${nodeName}`
|
||||
} else if (instructions) {
|
||||
return multiagent_supervisorPNG
|
||||
} else {
|
||||
return multiagent_workerPNG
|
||||
}
|
||||
}
|
||||
|
||||
// Get chatmessages successful
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data?.length) {
|
||||
@@ -654,6 +688,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
|
||||
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
|
||||
if (message.agentReasoning) obj.agentReasoning = JSON.parse(message.agentReasoning)
|
||||
if (message.action) obj.action = JSON.parse(message.action)
|
||||
if (message.fileUploads) {
|
||||
obj.fileUploads = JSON.parse(message.fileUploads)
|
||||
obj.fileUploads.forEach((file) => {
|
||||
@@ -778,6 +813,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
|
||||
socket.on('agentReasoning', updateLastMessageAgentReasoning)
|
||||
|
||||
socket.on('action', updateLastMessageAction)
|
||||
|
||||
socket.on('nextAgent', updateLastMessageNextAgent)
|
||||
|
||||
socket.on('abort', abortMessage)
|
||||
@@ -919,6 +956,15 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
setIsLeadSaving(false)
|
||||
}
|
||||
|
||||
const getInputDisabled = () => {
|
||||
return (
|
||||
loading ||
|
||||
!chatflowid ||
|
||||
(leadsConfig?.status && !isLeadSaved) ||
|
||||
(messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div onDragEnter={handleDrag}>
|
||||
{isDragActive && (
|
||||
@@ -981,31 +1027,6 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{message.usedTools && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{message.usedTools.map((tool, index) => {
|
||||
return tool ? (
|
||||
<Chip
|
||||
size='small'
|
||||
key={index}
|
||||
label={tool.tool}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconTool size={15} />}
|
||||
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.fileUploads && message.fileUploads.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
@@ -1117,11 +1138,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
height: '25px',
|
||||
width: 'auto'
|
||||
}}
|
||||
src={
|
||||
agent.instructions
|
||||
? multiagent_supervisorPNG
|
||||
: multiagent_workerPNG
|
||||
}
|
||||
src={getAgentIcon(agent.nodeName, agent.instructions)}
|
||||
alt='agentPNG'
|
||||
/>
|
||||
</Box>
|
||||
@@ -1152,6 +1169,26 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{agent.state && Object.keys(agent.state).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
size='small'
|
||||
label={'State'}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconDeviceSdCard size={15} />}
|
||||
onClick={() => onSourceDialogClick(agent.state, 'State')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{agent.messages.length > 0 && (
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
@@ -1221,6 +1258,31 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.usedTools && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{message.usedTools.map((tool, index) => {
|
||||
return tool ? (
|
||||
<Chip
|
||||
size='small'
|
||||
key={index}
|
||||
label={tool.tool}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconTool size={15} />}
|
||||
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className='markdownanswer'>
|
||||
{message.type === 'leadCaptureMessage' &&
|
||||
!getLocalStorageChatflow(chatflowid)?.lead &&
|
||||
@@ -1417,6 +1479,64 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.action && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{(message.action.elements || []).map((elem, index) => {
|
||||
return (
|
||||
<>
|
||||
{elem.type === 'approve-button' && elem.label === 'Yes' ? (
|
||||
<Button
|
||||
sx={{
|
||||
width: 'max-content',
|
||||
borderRadius: '20px',
|
||||
background: customization.isDarkMode ? 'transparent' : 'white'
|
||||
}}
|
||||
variant='outlined'
|
||||
color='success'
|
||||
key={index}
|
||||
startIcon={<IconCheck />}
|
||||
onClick={() => handleActionClick(elem, message.action)}
|
||||
>
|
||||
{elem.label}
|
||||
</Button>
|
||||
) : elem.type === 'reject-button' && elem.label === 'No' ? (
|
||||
<Button
|
||||
sx={{
|
||||
width: 'max-content',
|
||||
borderRadius: '20px',
|
||||
background: customization.isDarkMode ? 'transparent' : 'white'
|
||||
}}
|
||||
variant='outlined'
|
||||
color='error'
|
||||
key={index}
|
||||
startIcon={<IconX />}
|
||||
onClick={() => handleActionClick(elem, message.action)}
|
||||
>
|
||||
{elem.label}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
sx={{ width: 'max-content', borderRadius: '20px', background: 'white' }}
|
||||
variant='outlined'
|
||||
key={index}
|
||||
onClick={() => handleActionClick(elem, message.action)}
|
||||
>
|
||||
{elem.label}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
@@ -1546,7 +1666,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
// eslint-disable-next-line
|
||||
autoFocus
|
||||
sx={{ width: '100%' }}
|
||||
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
|
||||
disabled={getInputDisabled()}
|
||||
onKeyDown={handleEnter}
|
||||
id='userInput'
|
||||
name='userInput'
|
||||
@@ -1558,20 +1678,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
startAdornment={
|
||||
isChatFlowAvailableForUploads && (
|
||||
<InputAdornment position='start' sx={{ pl: 2 }}>
|
||||
<IconButton
|
||||
onClick={handleUploadClick}
|
||||
type='button'
|
||||
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
|
||||
edge='start'
|
||||
>
|
||||
<IconButton onClick={handleUploadClick} type='button' disabled={getInputDisabled()} edge='start'>
|
||||
<IconPhotoPlus
|
||||
color={
|
||||
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
|
||||
? '#9e9e9e'
|
||||
: customization.isDarkMode
|
||||
? 'white'
|
||||
: '#1e88e5'
|
||||
}
|
||||
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
@@ -1584,29 +1693,19 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
<IconButton
|
||||
onClick={() => onMicrophonePressed()}
|
||||
type='button'
|
||||
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
|
||||
disabled={getInputDisabled()}
|
||||
edge='end'
|
||||
>
|
||||
<IconMicrophone
|
||||
className={'start-recording-button'}
|
||||
color={
|
||||
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
|
||||
? '#9e9e9e'
|
||||
: customization.isDarkMode
|
||||
? 'white'
|
||||
: '#1e88e5'
|
||||
}
|
||||
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)}
|
||||
{!isAgentCanvas && (
|
||||
<InputAdornment position='end' sx={{ padding: '15px' }}>
|
||||
<IconButton
|
||||
type='submit'
|
||||
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
|
||||
edge='end'
|
||||
>
|
||||
<IconButton type='submit' disabled={getInputDisabled()} edge='end'>
|
||||
{loading ? (
|
||||
<div>
|
||||
<CircularProgress color='inherit' size={20} />
|
||||
@@ -1615,11 +1714,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
// Send icon SVG in input field
|
||||
<IconSend
|
||||
color={
|
||||
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
|
||||
? '#9e9e9e'
|
||||
: customization.isDarkMode
|
||||
? 'white'
|
||||
: '#1e88e5'
|
||||
getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -1630,14 +1725,10 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
<>
|
||||
{!loading && (
|
||||
<InputAdornment position='end' sx={{ padding: '15px' }}>
|
||||
<IconButton
|
||||
type='submit'
|
||||
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
|
||||
edge='end'
|
||||
>
|
||||
<IconButton type='submit' disabled={getInputDisabled()} edge='end'>
|
||||
<IconSend
|
||||
color={
|
||||
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
|
||||
getInputDisabled()
|
||||
? '#9e9e9e'
|
||||
: customization.isDarkMode
|
||||
? 'white'
|
||||
|
||||
@@ -46,7 +46,9 @@ const MarketplaceCanvas = () => {
|
||||
}, [flowData])
|
||||
|
||||
const onChatflowCopy = (flowData) => {
|
||||
const isAgentCanvas = (flowData?.nodes || []).some((node) => node.data.category === 'Multi Agents')
|
||||
const isAgentCanvas = (flowData?.nodes || []).some(
|
||||
(node) => node.data.category === 'Multi Agents' || node.data.category === 'Sequential Agents'
|
||||
)
|
||||
const templateFlowData = JSON.stringify(flowData)
|
||||
navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}`, { state: { templateFlowData } })
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ const HowToUseFunctionDialog = ({ show, onCancel }) => {
|
||||
<li>
|
||||
<code>$flow.input</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>$flow.state</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style={{ marginTop: 10 }}>
|
||||
|
||||
@@ -33,7 +33,7 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
const exampleAPIFunc = `/*
|
||||
* You can use any libraries imported in Flowise
|
||||
* You can use properties specified in Input Schema as variables. Ex: Property = userid, Variable = $userid
|
||||
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input
|
||||
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input, $flow.state
|
||||
* You can get custom variables: $vars.<variable-name>
|
||||
* Must return a string value at the end of function
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user