mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 17:01:00 +03:00
Add feature to be able to chain prompt values
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { createContext, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getUniqueNodeId } from 'utils/genericHelper'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const initialValue = {
|
||||
reactFlowInstance: null,
|
||||
setReactFlowInstance: () => {},
|
||||
duplicateNode: () => {},
|
||||
deleteNode: () => {},
|
||||
deleteEdge: () => {}
|
||||
}
|
||||
@@ -22,13 +25,53 @@ export const ReactFlowContext = ({ children }) => {
|
||||
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((edge) => edge.id !== id))
|
||||
}
|
||||
|
||||
const duplicateNode = (id) => {
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
const originalNode = nodes.find((n) => n.id === id)
|
||||
if (originalNode) {
|
||||
const newNodeId = getUniqueNodeId(originalNode.data, nodes)
|
||||
const clonedNode = cloneDeep(originalNode)
|
||||
|
||||
const duplicatedNode = {
|
||||
...clonedNode,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: clonedNode.position.x + 400,
|
||||
y: clonedNode.position.y
|
||||
},
|
||||
positionAbsolute: {
|
||||
x: clonedNode.positionAbsolute.x + 400,
|
||||
y: clonedNode.positionAbsolute.y
|
||||
},
|
||||
data: {
|
||||
...clonedNode.data,
|
||||
id: newNodeId
|
||||
},
|
||||
selected: false
|
||||
}
|
||||
|
||||
const dataKeys = ['inputParams', 'inputAnchors', 'outputAnchors']
|
||||
|
||||
for (const key of dataKeys) {
|
||||
for (const item of duplicatedNode.data[key]) {
|
||||
if (item.id) {
|
||||
item.id = item.id.replace(id, newNodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactFlowInstance.setNodes([...nodes, duplicatedNode])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<flowContext.Provider
|
||||
value={{
|
||||
reactFlowInstance,
|
||||
setReactFlowInstance,
|
||||
deleteNode,
|
||||
deleteEdge
|
||||
deleteEdge,
|
||||
duplicateNode
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.editor__textarea {
|
||||
outline: 0;
|
||||
}
|
||||
.editor__textarea::placeholder {
|
||||
color: rgba(120, 120, 120, 0.5);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Box,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Stack
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
|
||||
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
|
||||
|
||||
import './EditPromptValuesDialog.css'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const EditPromptValuesDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const languageType = 'json'
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
const [textCursorPosition, setTextCursorPosition] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) setInputValue(dialogProps.value)
|
||||
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
setInputParam(null)
|
||||
setTextCursorPosition({})
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
const onMouseUp = (e) => {
|
||||
if (e.target && e.target.selectionEnd && e.target.value) {
|
||||
const cursorPosition = e.target.selectionEnd
|
||||
const textBeforeCursorPosition = e.target.value.substring(0, cursorPosition)
|
||||
const textAfterCursorPosition = e.target.value.substring(cursorPosition, e.target.value.length)
|
||||
const body = {
|
||||
textBeforeCursorPosition,
|
||||
textAfterCursorPosition
|
||||
}
|
||||
setTextCursorPosition(body)
|
||||
} else {
|
||||
setTextCursorPosition({})
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectOutputResponseClick = (node, isUserQuestion = false) => {
|
||||
let variablePath = isUserQuestion ? `question` : `${node.id}.data.instance`
|
||||
if (textCursorPosition) {
|
||||
let newInput = ''
|
||||
if (textCursorPosition.textBeforeCursorPosition === undefined && textCursorPosition.textAfterCursorPosition === undefined)
|
||||
newInput = `${inputValue}${`{{${variablePath}}}`}`
|
||||
else newInput = `${textCursorPosition.textBeforeCursorPosition}{{${variablePath}}}${textCursorPosition.textAfterCursorPosition}`
|
||||
setInputValue(newInput)
|
||||
}
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogContent>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{inputParam && inputParam.type === 'string' && (
|
||||
<div style={{ flex: 70 }}>
|
||||
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
|
||||
{inputParam.label}
|
||||
</Typography>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.grey['500'],
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
{customization.isDarkMode ? (
|
||||
<DarkCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
onMouseUp={(e) => onMouseUp(e)}
|
||||
onBlur={(e) => onMouseUp(e)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LightCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
onMouseUp={(e) => onMouseUp(e)}
|
||||
onBlur={(e) => onMouseUp(e)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
{!dialogProps.disabled && inputParam && inputParam.acceptVariable && (
|
||||
<div style={{ flex: 30 }}>
|
||||
<Stack flexDirection='row' sx={{ mb: 1, ml: 2 }}>
|
||||
<Typography variant='h4'>Select Variable</Typography>
|
||||
</Stack>
|
||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 220px)', overflowX: 'hidden' }}>
|
||||
<Box sx={{ pl: 2, pr: 2 }}>
|
||||
<List>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={dialogProps.disabled}
|
||||
onClick={() => onSelectOutputResponseClick(null, true)}
|
||||
>
|
||||
<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='AI'
|
||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary='question'
|
||||
secondary={`User's question from chatbox`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
{dialogProps.availableNodesForVariable &&
|
||||
dialogProps.availableNodesForVariable.length > 0 &&
|
||||
dialogProps.availableNodesForVariable.map((node, index) => {
|
||||
const selectedOutputAnchor = node.data.outputAnchors[0].options.find(
|
||||
(ancr) => ancr.name === node.data.outputs['output']
|
||||
)
|
||||
return (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={dialogProps.disabled}
|
||||
onClick={() => onSelectOutputResponseClick(node)}
|
||||
>
|
||||
<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={node.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={
|
||||
node.data.inputs.chainName ? node.data.inputs.chainName : node.data.id
|
||||
}
|
||||
secondary={`${selectedOutputAnchor?.label ?? 'output'} from ${
|
||||
node.data.label
|
||||
}`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(inputValue, inputParam.name)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
EditPromptValuesDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default EditPromptValuesDialog
|
||||
@@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({
|
||||
}
|
||||
})
|
||||
|
||||
export const Dropdown = ({ name, value, options, onSelect, disabled = false }) => {
|
||||
export const Dropdown = ({ name, value, options, onSelect, disabled = false, disableClearable = false }) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
|
||||
const getDefaultOptionValue = () => ''
|
||||
@@ -29,6 +29,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false }) =
|
||||
<Autocomplete
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
disableClearable={disableClearable}
|
||||
size='small'
|
||||
options={options || []}
|
||||
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
|
||||
@@ -59,5 +60,6 @@ Dropdown.propTypes = {
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onSelect: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
disabled: PropTypes.bool,
|
||||
disableClearable: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import './prism-dark.css'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
export const DarkCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Editor
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
|
||||
@@ -32,6 +33,7 @@ export const DarkCodeEditor = ({ value, placeholder, type, style, onValueChange,
|
||||
DarkCodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onValueChange: PropTypes.func,
|
||||
|
||||
@@ -8,11 +8,12 @@ import './prism-light.css'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
export const LightCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Editor
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
|
||||
@@ -32,6 +33,7 @@ export const LightCodeEditor = ({ value, placeholder, type, style, onValueChange
|
||||
LightCodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onValueChange: PropTypes.func,
|
||||
|
||||
@@ -1,28 +1,53 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControl, OutlinedInput } from '@mui/material'
|
||||
import EditPromptValuesDialog from 'ui-component/dialog/EditPromptValuesDialog'
|
||||
|
||||
export const Input = ({ inputParam, value, onChange, disabled = false }) => {
|
||||
export const Input = ({ inputParam, value, onChange, disabled = false, showDialog, dialogProps, onDialogCancel, onDialogConfirm }) => {
|
||||
const [myValue, setMyValue] = useState(value ?? '')
|
||||
|
||||
const getInputType = (type) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'text'
|
||||
case 'password':
|
||||
return 'password'
|
||||
case 'number':
|
||||
return 'number'
|
||||
default:
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
<OutlinedInput
|
||||
id={inputParam.name}
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
type={inputParam.type === 'string' ? 'text' : inputParam.type}
|
||||
placeholder={inputParam.placeholder}
|
||||
multiline={!!inputParam.rows}
|
||||
maxRows={inputParam.rows || 0}
|
||||
minRows={inputParam.rows || 0}
|
||||
value={myValue}
|
||||
name={inputParam.name}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value)
|
||||
onChange(e.target.value)
|
||||
<>
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
<OutlinedInput
|
||||
id={inputParam.name}
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
type={getInputType(inputParam.type)}
|
||||
placeholder={inputParam.placeholder}
|
||||
multiline={!!inputParam.rows}
|
||||
rows={inputParam.rows ?? 1}
|
||||
value={myValue}
|
||||
name={inputParam.name}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value)
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<EditPromptValuesDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={onDialogCancel}
|
||||
onConfirm={(newValue, inputParamName) => {
|
||||
setMyValue(newValue)
|
||||
onDialogConfirm(newValue, inputParamName)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
></EditPromptValuesDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,5 +55,9 @@ Input.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
disabled: PropTypes.bool,
|
||||
showDialog: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onDialogCancel: PropTypes.func,
|
||||
onDialogConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
@@ -9,13 +9,9 @@ export const TooltipWithParser = ({ title }) => {
|
||||
|
||||
return (
|
||||
<Tooltip title={parser(title)} placement='right'>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<IconButton sx={{ height: 25, width: 25 }}>
|
||||
<Info
|
||||
style={{ background: 'transparent', color: customization.isDarkMode ? 'white' : 'inherit', height: 18, width: 18 }}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton sx={{ height: 25, width: 25 }}>
|
||||
<Info style={{ background: 'transparent', color: customization.isDarkMode ? 'white' : 'inherit', height: 18, width: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,23 +22,12 @@ export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
return nodeId
|
||||
}
|
||||
|
||||
export const initializeNodeData = (nodeParams) => {
|
||||
export const initializeDefaultNodeData = (nodeParams) => {
|
||||
const initialValues = {}
|
||||
|
||||
for (let i = 0; i < nodeParams.length; i += 1) {
|
||||
const input = nodeParams[i]
|
||||
|
||||
// Load from nodeParams default values
|
||||
initialValues[input.name] = input.default || ''
|
||||
|
||||
// Special case for array, always initialize the item if default is not set
|
||||
if (input.type === 'array' && !input.default) {
|
||||
const newObj = {}
|
||||
for (let j = 0; j < input.array.length; j += 1) {
|
||||
newObj[input.array[j].name] = input.array[j].default || ''
|
||||
}
|
||||
initialValues[input.name] = [newObj]
|
||||
}
|
||||
}
|
||||
|
||||
return initialValues
|
||||
@@ -46,62 +35,118 @@ export const initializeNodeData = (nodeParams) => {
|
||||
|
||||
export const initNode = (nodeData, newNodeId) => {
|
||||
const inputAnchors = []
|
||||
const inputParams = []
|
||||
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
|
||||
const outgoing = 1
|
||||
|
||||
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
|
||||
const whitelistTypes = ['options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
|
||||
|
||||
for (let i = 0; i < incoming; i += 1) {
|
||||
if (whitelistTypes.includes(nodeData.inputs[i].type)) continue
|
||||
const newInput = {
|
||||
...nodeData.inputs[i],
|
||||
id: `${newNodeId}-input-${nodeData.inputs[i].name}-${nodeData.inputs[i].type}`
|
||||
}
|
||||
inputAnchors.push(newInput)
|
||||
if (whitelistTypes.includes(nodeData.inputs[i].type)) {
|
||||
inputParams.push(newInput)
|
||||
} else {
|
||||
inputAnchors.push(newInput)
|
||||
}
|
||||
}
|
||||
|
||||
const outputAnchors = []
|
||||
for (let i = 0; i < outgoing; i += 1) {
|
||||
const newOutput = {
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
if (nodeData.outputs && nodeData.outputs.length) {
|
||||
const options = []
|
||||
for (let j = 0; j < nodeData.outputs.length; j += 1) {
|
||||
let baseClasses = ''
|
||||
let type = ''
|
||||
|
||||
if (whitelistTypes.includes(nodeData.outputs[j].type)) {
|
||||
baseClasses = nodeData.outputs[j].type
|
||||
type = nodeData.outputs[j].type
|
||||
} else {
|
||||
baseClasses = nodeData.baseClasses.join('|')
|
||||
type = nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
|
||||
const newOutputOption = {
|
||||
id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`,
|
||||
name: nodeData.outputs[j].name,
|
||||
label: nodeData.outputs[j].label,
|
||||
type
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
}
|
||||
const newOutput = {
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
options,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
} else {
|
||||
const newOutput = {
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
}
|
||||
|
||||
nodeData.id = newNodeId
|
||||
nodeData.inputAnchors = inputAnchors
|
||||
nodeData.outputAnchors = outputAnchors
|
||||
|
||||
/*
|
||||
Initial inputs = [
|
||||
/* Initial
|
||||
inputs = [
|
||||
{
|
||||
label: 'field_label',
|
||||
name: 'field'
|
||||
label: 'field_label_1',
|
||||
name: 'string'
|
||||
},
|
||||
{
|
||||
label: 'field_label_2',
|
||||
name: 'CustomType'
|
||||
}
|
||||
]
|
||||
|
||||
// Turn into inputs object with default values
|
||||
Converted inputs = { 'field': 'defaultvalue' }
|
||||
=> Convert to inputs, inputParams, inputAnchors
|
||||
|
||||
=> inputs = { 'field': 'defaultvalue' } // Turn into inputs object with default values
|
||||
|
||||
// Move remaining inputs that are not part of inputAnchors to inputParams
|
||||
inputParams = [
|
||||
{
|
||||
label: 'field_label',
|
||||
name: 'field'
|
||||
}
|
||||
]
|
||||
=> // For inputs that are part of whitelistTypes
|
||||
inputParams = [
|
||||
{
|
||||
label: 'field_label_1',
|
||||
name: 'string'
|
||||
}
|
||||
]
|
||||
|
||||
=> // For inputs that are not part of whitelistTypes
|
||||
inputAnchors = [
|
||||
{
|
||||
label: 'field_label_2',
|
||||
name: 'CustomType'
|
||||
}
|
||||
]
|
||||
*/
|
||||
if (nodeData.inputs) {
|
||||
nodeData.inputParams = nodeData.inputs.filter(({ name }) => !nodeData.inputAnchors.some((exclude) => exclude.name === name))
|
||||
nodeData.inputs = initializeNodeData(nodeData.inputs)
|
||||
nodeData.inputAnchors = inputAnchors
|
||||
nodeData.inputParams = inputParams
|
||||
nodeData.inputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
} else {
|
||||
nodeData.inputAnchors = []
|
||||
nodeData.inputParams = []
|
||||
nodeData.inputs = {}
|
||||
}
|
||||
|
||||
if (nodeData.outputs) {
|
||||
nodeData.outputs = initializeDefaultNodeData(outputAnchors)
|
||||
} else {
|
||||
nodeData.outputs = {}
|
||||
}
|
||||
|
||||
nodeData.outputAnchors = outputAnchors
|
||||
nodeData.id = newNodeId
|
||||
|
||||
return nodeData
|
||||
}
|
||||
|
||||
@@ -133,7 +178,9 @@ export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const targetNodeInputAnchor = targetNode.data.inputAnchors.find((ancr) => ancr.id === targetHandle)
|
||||
const targetNodeInputAnchor =
|
||||
targetNode.data.inputAnchors.find((ancr) => ancr.id === targetHandle) ||
|
||||
targetNode.data.inputParams.find((ancr) => ancr.id === targetHandle)
|
||||
if (
|
||||
(targetNodeInputAnchor &&
|
||||
!targetNodeInputAnchor?.list &&
|
||||
@@ -144,7 +191,6 @@ export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -200,6 +246,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
inputAnchors: node.data.inputAnchors,
|
||||
inputs: {},
|
||||
outputAnchors: node.data.outputAnchors,
|
||||
outputs: node.data.outputs,
|
||||
selected: false
|
||||
}
|
||||
|
||||
@@ -225,11 +272,16 @@ export const generateExportFlowData = (flowData) => {
|
||||
return exportJson
|
||||
}
|
||||
|
||||
export const copyToClipboard = (e) => {
|
||||
const src = e.src
|
||||
if (Array.isArray(src) || typeof src === 'object') {
|
||||
navigator.clipboard.writeText(JSON.stringify(src, null, ' '))
|
||||
} else {
|
||||
navigator.clipboard.writeText(src)
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) => {
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string-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)
|
||||
}
|
||||
}
|
||||
return parentNodes
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import NodeOutputHandler from './NodeOutputHandler'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { IconTrash } from '@tabler/icons'
|
||||
import { IconTrash, IconCopy } from '@tabler/icons'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
@@ -33,7 +33,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
|
||||
const CanvasNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const { deleteNode } = useContext(flowContext)
|
||||
const { deleteNode, duplicateNode } = useContext(flowContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -76,10 +76,22 @@ const CanvasNode = ({ data }) => {
|
||||
</Box>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<IconButton
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{ height: 35, width: 35, '&:hover': { color: theme?.palette.primary.main } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{ height: 35, width: 35, mr: 1 }}
|
||||
sx={{ height: 35, width: 35, mr: 1, '&:hover': { color: 'red' } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
|
||||
@@ -4,13 +4,16 @@ import { useEffect, useRef, useState, useContext } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import { Box, Typography, Tooltip } from '@mui/material'
|
||||
import { Box, Typography, Tooltip, IconButton } from '@mui/material'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { IconArrowsMaximize } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import { Input } from 'ui-component/input/Input'
|
||||
import { File } from 'ui-component/file/File'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
import { isValidConnection } from 'utils/genericHelper'
|
||||
import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper'
|
||||
|
||||
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
@@ -23,9 +26,35 @@ const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...prop
|
||||
const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false }) => {
|
||||
const theme = useTheme()
|
||||
const ref = useRef(null)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||
|
||||
const onExpandDialogClicked = (value, inputParam) => {
|
||||
const dialogProp = {
|
||||
value,
|
||||
inputParam,
|
||||
disabled,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
const nodesForVariable = inputParam.acceptVariable ? getAvailableNodesForVariable(nodes, edges, data.id, inputParam.id) : []
|
||||
dialogProp.availableNodesForVariable = nodesForVariable
|
||||
}
|
||||
setExpandDialogProps(dialogProp)
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
|
||||
const onExpandDialogSave = (newValue, inputParamName) => {
|
||||
setShowExpandDialog(false)
|
||||
data.inputs[inputParamName] = newValue
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
@@ -68,11 +97,47 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false }) =
|
||||
|
||||
{inputParam && (
|
||||
<>
|
||||
{inputParam.acceptVariable && (
|
||||
<CustomWidthTooltip placement='left' title={inputParam.type}>
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
key={inputParam.id}
|
||||
id={inputParam.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
)}
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
</Typography>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
{inputParam.type === 'string' && inputParam.rows && (
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{
|
||||
height: 25,
|
||||
width: 25
|
||||
}}
|
||||
title='Expand'
|
||||
color='primary'
|
||||
onClick={() =>
|
||||
onExpandDialogClicked(data.inputs[inputParam.name] ?? inputParam.default ?? '', inputParam)
|
||||
}
|
||||
>
|
||||
<IconArrowsMaximize />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{inputParam.type === 'file' && (
|
||||
<File
|
||||
disabled={disabled}
|
||||
@@ -87,6 +152,10 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false }) =
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
showDialog={showExpandDialog}
|
||||
dialogProps={expandDialogProps}
|
||||
onDialogCancel={() => setShowExpandDialog(false)}
|
||||
onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Box, Typography, Tooltip } from '@mui/material'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
import { isValidConnection } from 'utils/genericHelper'
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
|
||||
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
@@ -17,11 +18,12 @@ const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...prop
|
||||
|
||||
// ===========================|| NodeOutputHandler ||=========================== //
|
||||
|
||||
const NodeOutputHandler = ({ outputAnchor, data }) => {
|
||||
const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
|
||||
const theme = useTheme()
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const [dropdownValue, setDropdownValue] = useState(null)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,33 +41,82 @@ const NodeOutputHandler = ({ outputAnchor, data }) => {
|
||||
}, 0)
|
||||
}, [data.id, position, updateNodeInternals])
|
||||
|
||||
useEffect(() => {
|
||||
if (dropdownValue) {
|
||||
setTimeout(() => {
|
||||
updateNodeInternals(data.id)
|
||||
}, 0)
|
||||
}
|
||||
}, [data.id, dropdownValue, updateNodeInternals])
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<CustomWidthTooltip placement='right' title={outputAnchor.type}>
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={outputAnchor.id}
|
||||
id={outputAnchor.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
<Box sx={{ p: 2, textAlign: 'end' }}>
|
||||
<Typography>{outputAnchor.label}</Typography>
|
||||
</Box>
|
||||
{outputAnchor.type !== 'options' && !outputAnchor.options && (
|
||||
<>
|
||||
<CustomWidthTooltip placement='right' title={outputAnchor.type}>
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={outputAnchor.id}
|
||||
id={outputAnchor.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
<Box sx={{ p: 2, textAlign: 'end' }}>
|
||||
<Typography>{outputAnchor.label}</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{outputAnchor.type === 'options' && outputAnchor.options && outputAnchor.options.length > 0 && (
|
||||
<>
|
||||
<CustomWidthTooltip
|
||||
placement='right'
|
||||
title={
|
||||
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ?? outputAnchor.type
|
||||
}
|
||||
>
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
id={outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</CustomWidthTooltip>
|
||||
<Box sx={{ p: 2, textAlign: 'end' }}>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
disableClearable={true}
|
||||
name={outputAnchor.name}
|
||||
options={outputAnchor.options}
|
||||
onSelect={(newValue) => {
|
||||
setDropdownValue(newValue)
|
||||
data.outputs[outputAnchor.name] = newValue
|
||||
}}
|
||||
value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
NodeOutputHandler.propTypes = {
|
||||
outputAnchor: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
data: PropTypes.object,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default NodeOutputHandler
|
||||
|
||||
@@ -108,10 +108,14 @@ const Canvas = () => {
|
||||
setTimeout(() => setDirty(), 0)
|
||||
let value
|
||||
const inputAnchor = node.data.inputAnchors.find((ancr) => ancr.name === targetInput)
|
||||
const inputParam = node.data.inputParams.find((param) => param.name === targetInput)
|
||||
|
||||
if (inputAnchor && inputAnchor.list) {
|
||||
const newValues = node.data.inputs[targetInput] || []
|
||||
newValues.push(`{{${sourceNodeId}.data.instance}}`)
|
||||
value = newValues
|
||||
} else if (inputParam && inputParam.acceptVariable) {
|
||||
value = node.data.inputs[targetInput] || ''
|
||||
} else {
|
||||
value = `{{${sourceNodeId}.data.instance}}`
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const MarketplaceCanvasNode = ({ data }) => {
|
||||
</>
|
||||
)}
|
||||
{data.inputAnchors.map((inputAnchor, index) => (
|
||||
<NodeInputHandler key={index} inputAnchor={inputAnchor} data={data} />
|
||||
<NodeInputHandler disabled={true} key={index} inputAnchor={inputAnchor} data={data} />
|
||||
))}
|
||||
{data.inputParams.map((inputParam, index) => (
|
||||
<NodeInputHandler disabled={true} key={index} inputParam={inputParam} data={data} />
|
||||
@@ -108,7 +108,7 @@ const MarketplaceCanvasNode = ({ data }) => {
|
||||
<Divider />
|
||||
|
||||
{data.outputAnchors.map((outputAnchor, index) => (
|
||||
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
|
||||
<NodeOutputHandler disabled={true} key={index} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
|
||||
Reference in New Issue
Block a user