FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
parent
2fb9850af5
commit
7753ba2d37
1161 changed files with 103836 additions and 10327 deletions
|
|
@ -0,0 +1,28 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
text: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const AddButton: FC<Props> = ({
|
||||
className,
|
||||
text,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'flex items-center h-7 justify-center bg-gray-100 hover:bg-gray-200 rounded-lg cursor-pointer text-xs font-medium text-gray-700 space-x-1')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Plus className='w-3.5 h-3.5' />
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddButton)
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import type { InputVar } from '../../../../types'
|
||||
import { BlockEnum, InputVarType } from '../../../../types'
|
||||
import CodeEditor from '../editor/code-editor'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import Select from '@/app/components/base/select'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
payload: InputVar
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FormItem: FC<Props> = ({
|
||||
payload,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const handleContextItemChange = useCallback((index: number) => {
|
||||
return (newValue: any) => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft[index] = newValue
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [value, onChange])
|
||||
|
||||
const handleContextItemRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [value, onChange])
|
||||
const nodeKey = (() => {
|
||||
if (typeof payload.label === 'object') {
|
||||
const { nodeType, nodeName, variable } = payload.label
|
||||
return (
|
||||
<div className='h-full flex items-center'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon type={nodeType || BlockEnum.Start} />
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 max-w-[150px] truncate' title={nodeName}>
|
||||
{nodeName}
|
||||
</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='ml-0.5 text-xs font-medium max-w-[150px] truncate' title={variable} >
|
||||
{variable}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
{type !== InputVarType.contexts && <div className='h-8 leading-8 text-[13px] font-medium text-gray-700 truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>}
|
||||
<div className='grow'>
|
||||
{
|
||||
type === InputVarType.textInput && (
|
||||
<input
|
||||
className="w-full px-3 text-sm leading-8 text-gray-900 border-0 rounded-lg grow h-8 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.number && (
|
||||
<input
|
||||
className="w-full px-3 text-sm leading-8 text-gray-900 border-0 rounded-lg grow h-8 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.paragraph && (
|
||||
<textarea
|
||||
className="w-full px-3 py-1 text-sm leading-[18px] text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.select && (
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={value || ''}
|
||||
items={payload.options?.map(option => ({ name: option, value: option })) || []}
|
||||
onSelect={i => onChange(i.value)}
|
||||
allowSearch={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.json && (
|
||||
<CodeEditor
|
||||
value={value}
|
||||
title={<span>JSON</span>}
|
||||
language={CodeLanguage.json}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.files && (
|
||||
<TextGenerationImageUploader
|
||||
settings={{
|
||||
...fileSettings.image,
|
||||
detail: Resolution.high,
|
||||
}}
|
||||
onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.contexts && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<CodeEditor
|
||||
key={index}
|
||||
value={item}
|
||||
title={<span>JSON</span>}
|
||||
headerRight={
|
||||
(value as any).length > 1
|
||||
? (<Trash03
|
||||
onClick={handleContextItemRemove(index)}
|
||||
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleContextItemChange(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FormItem)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import type { InputVar } from '../../../../types'
|
||||
import FormItem from './form-item'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
|
||||
|
||||
export type Props = {
|
||||
className?: string
|
||||
label?: string
|
||||
inputs: InputVar[]
|
||||
values: Record<string, string>
|
||||
onChange: (newValues: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const Form: FC<Props> = ({
|
||||
className,
|
||||
label,
|
||||
inputs,
|
||||
values,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: any) => {
|
||||
const newValues = produce(values, (draft) => {
|
||||
draft[key] = value
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [values, onChange])
|
||||
|
||||
const handleAddContext = useCallback(() => {
|
||||
const newValues = produce(values, (draft: any) => {
|
||||
const key = inputs[0].variable
|
||||
draft[key].push(RETRIEVAL_OUTPUT_STRUCT)
|
||||
})
|
||||
onChange(newValues)
|
||||
}, [values, onChange, inputs])
|
||||
return (
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
{label && (
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>{label}</div>
|
||||
{inputs[0]?.type === InputVarType.contexts && (
|
||||
<AddButton onClick={handleAddContext} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{inputs.map((input, index) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={index}
|
||||
payload={input}
|
||||
value={values[input.variable]}
|
||||
onChange={handleChange(input.variable)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Form)
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { Props as FormProps } from './form'
|
||||
import Form from './form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { Loading02, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type BeforeRunFormProps = {
|
||||
nodeName: string
|
||||
onHide: () => void
|
||||
onRun: (submitData: Record<string, any>) => void
|
||||
onStop: () => void
|
||||
runningStatus: NodeRunningStatus
|
||||
result?: JSX.Element
|
||||
forms: FormProps[]
|
||||
}
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
if (type === InputVarType.number)
|
||||
return parseFloat(value)
|
||||
if (type === InputVarType.json)
|
||||
return JSON.parse(value)
|
||||
if (type === InputVarType.contexts) {
|
||||
return value.map((item: any) => {
|
||||
return JSON.parse(item)
|
||||
})
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
nodeName,
|
||||
onHide,
|
||||
onRun,
|
||||
onStop,
|
||||
runningStatus,
|
||||
result,
|
||||
forms,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
|
||||
const isRunning = runningStatus === NodeRunningStatus.Running
|
||||
|
||||
const handleRun = useCallback(() => {
|
||||
let errMsg = ''
|
||||
forms.forEach((form) => {
|
||||
form.inputs.forEach((input) => {
|
||||
const value = form.values[input.variable]
|
||||
if (!errMsg && input.required && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
|
||||
errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label })
|
||||
})
|
||||
})
|
||||
if (errMsg) {
|
||||
Toast.notify({
|
||||
message: errMsg,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const submitData: Record<string, any> = {}
|
||||
let parseErrorJsonField = ''
|
||||
forms.forEach((form) => {
|
||||
form.inputs.forEach((input) => {
|
||||
try {
|
||||
const value = formatValue(form.values[input.variable], input.type)
|
||||
submitData[input.variable] = value
|
||||
}
|
||||
catch (e) {
|
||||
parseErrorJsonField = input.variable
|
||||
}
|
||||
})
|
||||
})
|
||||
if (parseErrorJsonField) {
|
||||
Toast.notify({
|
||||
message: t('workflow.errorMsg.invalidJson', { field: parseErrorJsonField }),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
onRun(submitData)
|
||||
}, [forms, onRun, t])
|
||||
return (
|
||||
<div className='absolute inset-0 z-10 rounded-2xl pt-10' style={{
|
||||
backgroundColor: 'rgba(16, 24, 40, 0.20)',
|
||||
}}>
|
||||
<div className='h-full rounded-2xl bg-white flex flex-col'>
|
||||
<div className='shrink-0 flex justify-between items-center h-8 pl-4 pr-3 pt-3'>
|
||||
<div className='text-base font-semibold text-gray-900 truncate'>
|
||||
{t(`${i18nPrefix}.testRun`)} {nodeName}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
|
||||
<XClose className='w-4 h-4 text-gray-500 ' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<div className='mt-3 px-4 space-y-4'>
|
||||
{forms.map((form, index) => (
|
||||
<div key={index}>
|
||||
<Form
|
||||
key={index}
|
||||
className={cn(index < forms.length - 1 && 'mb-4')}
|
||||
{...form}
|
||||
/>
|
||||
{index < forms.length - 1 && <Split />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex justify-between space-x-2 px-4' >
|
||||
{isRunning && (
|
||||
<div
|
||||
className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer'
|
||||
onClick={onStop}
|
||||
>
|
||||
<StopCircle className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
)}
|
||||
<Button disabled={isRunning} type='primary' className='w-0 grow !h-8 flex items-center space-x-2 text-[13px]' onClick={handleRun}>
|
||||
{isRunning && <Loading02 className='animate-spin w-4 h-4 text-white' />}
|
||||
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
|
||||
</Button>
|
||||
</div>
|
||||
{isRunning && (
|
||||
<ResultPanel status='running' showSteps={false} />
|
||||
)}
|
||||
{isFinished && (
|
||||
<>
|
||||
{result}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(BeforeRunForm)
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import cn from 'classnames'
|
||||
import PromptEditorHeightResizeWrap from '@/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap'
|
||||
import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title: JSX.Element | string
|
||||
headerRight?: JSX.Element
|
||||
children: JSX.Element
|
||||
minHeight?: number
|
||||
value: string
|
||||
isFocus: boolean
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
headerRight,
|
||||
children,
|
||||
minHeight = 120,
|
||||
value,
|
||||
isFocus,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
wrapClassName,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
editorExpandHeight,
|
||||
} = useToggleExpend({ ref, hasFooter: false })
|
||||
|
||||
const editorContentMinHeight = minHeight - 28
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(editorContentMinHeight)
|
||||
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className={cn(wrapClassName)}>
|
||||
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'bg-white border-gray-200' : 'bg-gray-100 border-gray-100 overflow-hidden')}>
|
||||
<div className='flex justify-between items-center h-7 pt-1 pl-3 pr-2'>
|
||||
<div className='text-xs font-semibold text-gray-700'>{title}</div>
|
||||
<div className='flex items-center'>
|
||||
{headerRight}
|
||||
{!isCopied
|
||||
? (
|
||||
<Clipboard className='mx-1 w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleCopy} />
|
||||
)
|
||||
: (
|
||||
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-gray-500' />
|
||||
)
|
||||
}
|
||||
<div className='ml-1'>
|
||||
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PromptEditorHeightResizeWrap
|
||||
height={isExpand ? editorExpandHeight : editorContentHeight}
|
||||
minHeight={editorContentMinHeight}
|
||||
onHeightChange={setEditorContentHeight}
|
||||
hideResize={isExpand}
|
||||
>
|
||||
<div className='h-full pb-2'>
|
||||
{children}
|
||||
</div>
|
||||
</PromptEditorHeightResizeWrap>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Base)
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
import React, { useRef } from 'react'
|
||||
import Base from '../base'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import './style.css'
|
||||
|
||||
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
||||
loader.config({ paths: { vs: '/vs' } })
|
||||
|
||||
type Props = {
|
||||
value?: string | object
|
||||
onChange?: (value: string) => void
|
||||
title: JSX.Element
|
||||
language: CodeLanguage
|
||||
headerRight?: JSX.Element
|
||||
readOnly?: boolean
|
||||
isJSONStringifyBeauty?: boolean
|
||||
height?: number
|
||||
}
|
||||
|
||||
const languageMap = {
|
||||
[CodeLanguage.javascript]: 'javascript',
|
||||
[CodeLanguage.python3]: 'python',
|
||||
[CodeLanguage.json]: 'json',
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
value = '',
|
||||
onChange = () => { },
|
||||
title,
|
||||
headerRight,
|
||||
language,
|
||||
readOnly,
|
||||
isJSONStringifyBeauty,
|
||||
height,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
onChange(value || '')
|
||||
}
|
||||
|
||||
const editorRef = useRef(null)
|
||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
editor.onDidFocusEditorText(() => {
|
||||
setIsFocus(true)
|
||||
})
|
||||
editor.onDidBlurEditorText(() => {
|
||||
setIsFocus(false)
|
||||
})
|
||||
|
||||
monaco.editor.defineTheme('blur-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#F2F4F7',
|
||||
},
|
||||
})
|
||||
|
||||
monaco.editor.defineTheme('focus-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#ffffff',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const outPutValue = (() => {
|
||||
if (!isJSONStringifyBeauty)
|
||||
return value as string
|
||||
try {
|
||||
return JSON.stringify(value as object, null, 2)
|
||||
}
|
||||
catch (e) {
|
||||
return value as string
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Base
|
||||
title={title}
|
||||
value={outPutValue}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={height || 200}
|
||||
>
|
||||
<>
|
||||
{/* https://www.npmjs.com/package/@monaco-editor/react */}
|
||||
<Editor
|
||||
className='h-full'
|
||||
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
|
||||
language={languageMap[language] || 'javascript'}
|
||||
theme={isFocus ? 'focus-theme' : 'blur-theme'}
|
||||
value={outPutValue}
|
||||
onChange={handleEditorChange}
|
||||
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
quickSuggestions: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 1, // would change line num width
|
||||
wordWrap: 'on', // auto line wrap
|
||||
// lineNumbers: (num) => {
|
||||
// return <div>{num}</div>
|
||||
// }
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
</>
|
||||
</Base>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeEditor)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.margin-view-overlays {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* hide readonly tooltip */
|
||||
.monaco-editor-overlaymessage {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Base from './base'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
title: JSX.Element | string
|
||||
headerRight?: JSX.Element
|
||||
minHeight?: number
|
||||
onBlur?: () => void
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const TextEditor: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
title,
|
||||
headerRight,
|
||||
minHeight,
|
||||
onBlur,
|
||||
placeholder,
|
||||
readonly,
|
||||
}) => {
|
||||
const [isFocus, {
|
||||
setTrue: setIsFocus,
|
||||
setFalse: setIsNotFocus,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setIsNotFocus()
|
||||
onBlur?.()
|
||||
}, [setIsNotFocus, onBlur])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Base
|
||||
title={title}
|
||||
value={value}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus}
|
||||
minHeight={minHeight}
|
||||
>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onFocus={setIsFocus}
|
||||
onBlur={handleBlur}
|
||||
className='w-full h-full px-3 resize-none bg-transparent border-none focus:outline-none leading-[18px] text-[13px] font-normal text-gray-900 placeholder:text-gray-300'
|
||||
placeholder={placeholder}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</Base>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TextEditor)
|
||||
57
web/app/components/workflow/nodes/_base/components/field.tsx
Normal file
57
web/app/components/workflow/nodes/_base/components/field.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
type Props = {
|
||||
title: string
|
||||
tooltip?: string
|
||||
supportFold?: boolean
|
||||
children?: JSX.Element | string | null
|
||||
operations?: JSX.Element
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
const Filed: FC<Props> = ({
|
||||
title,
|
||||
tooltip,
|
||||
children,
|
||||
operations,
|
||||
inline,
|
||||
supportFold,
|
||||
}) => {
|
||||
const [fold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(true)
|
||||
return (
|
||||
<div className={cn(inline && 'flex justify-between items-center', supportFold && 'cursor-pointer')}>
|
||||
<div
|
||||
onClick={() => supportFold && toggleFold()}
|
||||
className='flex justify-between items-center'>
|
||||
<div className='flex items-center h-6'>
|
||||
<div className='text-[13px] font-medium text-gray-700 uppercase'>{title}</div>
|
||||
{tooltip && (
|
||||
<TooltipPlus popupContent={
|
||||
<div className='w-[120px]'>
|
||||
{tooltip}
|
||||
</div>}>
|
||||
<HelpCircle className='w-3.5 h-3.5 ml-0.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className='flex'>
|
||||
{operations && <div>{operations}</div>}
|
||||
{supportFold && (
|
||||
<ChevronRight className='w-3.5 h-3.5 text-gray-500 cursor-pointer transform transition-transform' style={{ transform: fold ? 'rotate(0deg)' : 'rotate(90deg)' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children && (!supportFold || (supportFold && !fold)) && <div className={cn(!inline && 'mt-1')}>{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Filed)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
content: string | JSX.Element
|
||||
}
|
||||
|
||||
const InfoPanel: FC<Props> = ({
|
||||
title,
|
||||
content,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='px-[5px] py-[3px] bg-gray-100 rounded-md'>
|
||||
<div className='leading-4 text-[10px] font-medium text-gray-500 uppercase'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='leading-4 text-xs font-normal text-gray-700'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InfoPanel)
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type Props = {
|
||||
instanceId?: string
|
||||
className?: string
|
||||
placeholder?: string
|
||||
placeholderClassName?: string
|
||||
promptMinHeightClassName?: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onFocusChange?: (value: boolean) => void
|
||||
readOnly?: boolean
|
||||
justVar?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
promptMinHeightClassName = 'min-h-[20px]',
|
||||
value,
|
||||
onChange,
|
||||
onFocusChange,
|
||||
readOnly,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean(false)
|
||||
|
||||
useEffect(() => {
|
||||
onFocusChange?.(isFocus)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFocus])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'relative')}>
|
||||
<>
|
||||
<PromptEditor
|
||||
instanceId={instanceId}
|
||||
className={cn(promptMinHeightClassName, '!leading-[18px]')}
|
||||
placeholder={placeholder}
|
||||
placeholderClassName={placeholderClassName}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
datasets: [],
|
||||
onAddContext: () => { },
|
||||
}}
|
||||
historyBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
onEditRole: () => { },
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!readOnly}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
{isFocus && (
|
||||
<div className='absolute z-10 top-[-9px] right-1'>
|
||||
<TooltipPlus
|
||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||
>
|
||||
<div className='p-0.5 rounded-[5px] shadow-lg cursor-pointer bg-white hover:bg-gray-100 border-[0.5px] border-black/5'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-500' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(Editor)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { InputVarType } from '../../../types'
|
||||
import { AlignLeft, LetterSpacing01 } from '@/app/components/base/icons/src/vender/line/editor'
|
||||
import { CheckDone01, Hash02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
type: InputVarType
|
||||
}
|
||||
|
||||
const getIcon = (type: InputVarType) => {
|
||||
return ({
|
||||
[InputVarType.textInput]: LetterSpacing01,
|
||||
[InputVarType.paragraph]: AlignLeft,
|
||||
[InputVarType.select]: CheckDone01,
|
||||
[InputVarType.number]: Hash02,
|
||||
} as any)[type] || LetterSpacing01
|
||||
}
|
||||
|
||||
const InputVarTypeIcon: FC<Props> = ({
|
||||
className,
|
||||
type,
|
||||
}) => {
|
||||
const Icon = getIcon(type)
|
||||
return (
|
||||
<Icon className={className} />
|
||||
)
|
||||
}
|
||||
export default React.memo(InputVarTypeIcon)
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import type { Memory } from '../../../types'
|
||||
import { MemoryRole } from '../../../types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.common.memory'
|
||||
const WINDOW_SIZE_MIN = 1
|
||||
const WINDOW_SIZE_MAX = 100
|
||||
const WINDOW_SIZE_DEFAULT = 50
|
||||
type RoleItemProps = {
|
||||
readonly: boolean
|
||||
title: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const RoleItem: FC<RoleItemProps> = ({
|
||||
readonly,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-[13px] font-normal text-gray-700'>{title}</div>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className='w-[200px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
readonly: boolean
|
||||
config: { data?: Memory }
|
||||
onChange: (memory?: Memory) => void
|
||||
canSetRoleName?: boolean
|
||||
}
|
||||
|
||||
const MEMORY_DEFAULT: Memory = { window: { enabled: false, size: WINDOW_SIZE_DEFAULT } }
|
||||
|
||||
const MemoryConfig: FC<Props> = ({
|
||||
className,
|
||||
readonly,
|
||||
config = { data: MEMORY_DEFAULT },
|
||||
onChange,
|
||||
canSetRoleName = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const payload = config.data
|
||||
const handleMemoryEnabledChange = useCallback((enabled: boolean) => {
|
||||
onChange(enabled ? MEMORY_DEFAULT : undefined)
|
||||
}, [onChange])
|
||||
const handleWindowEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newPayload = produce(config.data || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.window)
|
||||
draft.window = { enabled: false, size: WINDOW_SIZE_DEFAULT }
|
||||
|
||||
draft.window.enabled = enabled
|
||||
})
|
||||
|
||||
onChange(newPayload)
|
||||
}, [config, onChange])
|
||||
|
||||
const handleWindowSizeChange = useCallback((size: number | string) => {
|
||||
const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.window)
|
||||
draft.window = { enabled: true, size: WINDOW_SIZE_DEFAULT }
|
||||
let limitedSize: null | string | number = size
|
||||
if (limitedSize === '') {
|
||||
limitedSize = null
|
||||
}
|
||||
else {
|
||||
limitedSize = parseInt(limitedSize as string, 10)
|
||||
if (isNaN(limitedSize))
|
||||
limitedSize = WINDOW_SIZE_DEFAULT
|
||||
|
||||
if (limitedSize < WINDOW_SIZE_MIN)
|
||||
limitedSize = WINDOW_SIZE_MIN
|
||||
|
||||
if (limitedSize > WINDOW_SIZE_MAX)
|
||||
limitedSize = WINDOW_SIZE_MAX
|
||||
}
|
||||
|
||||
draft.window.size = limitedSize as number
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [payload, onChange])
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
const payload = config.data
|
||||
if (!payload)
|
||||
return
|
||||
|
||||
if (payload.window.size === '' || payload.window.size === null)
|
||||
handleWindowSizeChange(WINDOW_SIZE_DEFAULT)
|
||||
}, [handleWindowSizeChange, config])
|
||||
|
||||
const handleRolePrefixChange = useCallback((role: MemoryRole) => {
|
||||
return (value: string) => {
|
||||
const newPayload = produce(config.data || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.role_prefix) {
|
||||
draft.role_prefix = {
|
||||
user: '',
|
||||
assistant: '',
|
||||
}
|
||||
}
|
||||
draft.role_prefix[role] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [config, onChange])
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.memory`)}
|
||||
tooltip={t(`${i18nPrefix}.memoryTip`)!}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={!!payload}
|
||||
onChange={handleMemoryEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{payload && (
|
||||
<>
|
||||
{/* window size */}
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex items-center h-8 space-x-1'>
|
||||
<Switch
|
||||
defaultValue={payload?.window?.enabled}
|
||||
onChange={handleWindowEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='leading-[18px] text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.windowSize`)}</div>
|
||||
</div>
|
||||
<div className='flex items-center h-8 space-x-2'>
|
||||
<Slider
|
||||
className='w-[144px]'
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={handleWindowSizeChange}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
<input
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
className='shrink-0 block ml-4 pl-3 w-12 h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
|
||||
type='number'
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={e => handleWindowSizeChange(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{canSetRoleName && (
|
||||
<div className='mt-4'>
|
||||
<div className='leading-6 text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.conversationRoleName`)}</div>
|
||||
<div className='mt-1 space-y-2'>
|
||||
<RoleItem
|
||||
readonly={readonly}
|
||||
title={t(`${i18nPrefix}.user`)}
|
||||
value={payload.role_prefix?.user || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.user)}
|
||||
/>
|
||||
<RoleItem
|
||||
readonly={readonly}
|
||||
title={t(`${i18nPrefix}.assistant`)}
|
||||
value={payload.role_prefix?.assistant || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.assistant)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(MemoryConfig)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type AddProps = {
|
||||
nodeId: string
|
||||
nodeType: BlockEnum
|
||||
sourceHandle: string
|
||||
branchName?: string
|
||||
}
|
||||
const Add = ({
|
||||
nodeId,
|
||||
nodeType,
|
||||
sourceHandle,
|
||||
branchName,
|
||||
}: AddProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const availableNextNodes = nodesExtraData[nodeType].availableNextNodes
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: nodeId,
|
||||
prevNodeSourceHandle: sourceHandle,
|
||||
},
|
||||
)
|
||||
}, [nodeId, sourceHandle, handleNodeAdd])
|
||||
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative flex items-center px-2 h-9 rounded-lg border border-dashed border-gray-200 bg-gray-50
|
||||
hover:bg-gray-100 text-xs text-gray-500 cursor-pointer
|
||||
${open && '!bg-gray-100'}
|
||||
${nodesReadOnly && '!cursor-not-allowed'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-gray-500 font-semibold'
|
||||
title={branchName.toLocaleUpperCase()}
|
||||
>
|
||||
<div className='inline-block px-0.5 rounded-[5px] bg-white truncate'>{branchName.toLocaleUpperCase()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-gray-200'>
|
||||
<Plus className='w-3 h-3' />
|
||||
</div>
|
||||
{t('workflow.panel.selectNextStep')}
|
||||
</div>
|
||||
)
|
||||
}, [branchName, t, nodesReadOnly])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
placement='top'
|
||||
offset={0}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={availableNextNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Add)
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { memo } from 'react'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useEdges,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useToolIcon } from '../../../../hooks'
|
||||
import BlockIcon from '../../../../block-icon'
|
||||
import type {
|
||||
Branch,
|
||||
Node,
|
||||
} from '../../../../types'
|
||||
import { BlockEnum } from '../../../../types'
|
||||
import Add from './add'
|
||||
import Item from './item'
|
||||
import Line from './line'
|
||||
|
||||
type NextStepProps = {
|
||||
selectedNode: Node
|
||||
}
|
||||
const NextStep = ({
|
||||
selectedNode,
|
||||
}: NextStepProps) => {
|
||||
const data = selectedNode.data
|
||||
const toolIcon = useToolIcon(data)
|
||||
const store = useStoreApi()
|
||||
const branches = data._targetBranches || []
|
||||
const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
|
||||
const edges = useEdges()
|
||||
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
|
||||
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
|
||||
|
||||
return (
|
||||
<div className='flex py-1'>
|
||||
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-xs'>
|
||||
<BlockIcon
|
||||
type={selectedNode!.data.type}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
</div>
|
||||
<Line linesNumber={nodeWithBranches ? branches.length : 1} />
|
||||
<div className='grow'>
|
||||
{
|
||||
!nodeWithBranches && !!outgoers.length && (
|
||||
<Item
|
||||
nodeId={outgoers[0].id}
|
||||
data={outgoers[0].data}
|
||||
sourceHandle='source'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodeWithBranches && !outgoers.length && (
|
||||
<Add
|
||||
nodeId={selectedNode!.id}
|
||||
nodeType={selectedNode!.data.type}
|
||||
sourceHandle='source'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!branches?.length && nodeWithBranches && (
|
||||
branches.map((branch: Branch) => {
|
||||
const connected = connectedEdges.find(edge => edge.sourceHandle === branch.id)
|
||||
const target = outgoers.find(outgoer => outgoer.id === connected?.target)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={branch.id}
|
||||
className='mb-3 last-of-type:mb-0'
|
||||
>
|
||||
{
|
||||
connected && (
|
||||
<Item
|
||||
data={target!.data!}
|
||||
nodeId={target!.id}
|
||||
sourceHandle={branch.id}
|
||||
branchName={branch.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!connected && (
|
||||
<Add
|
||||
key={branch.id}
|
||||
nodeId={selectedNode!.id}
|
||||
nodeType={selectedNode!.data.type}
|
||||
sourceHandle={branch.id}
|
||||
branchName={branch.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NextStep)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { intersection } from 'lodash-es'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
branchName?: string
|
||||
data: CommonNodeType
|
||||
}
|
||||
const Item = ({
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
branchName,
|
||||
data,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const toolIcon = useToolIcon(data)
|
||||
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
|
||||
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
className={`
|
||||
hidden group-hover:flex px-2 py-0 h-6 bg-white text-xs text-gray-700 font-medium rounded-md
|
||||
${open && '!bg-gray-100 !flex'}
|
||||
`}
|
||||
>
|
||||
{t('workflow.panel.change')}
|
||||
</Button>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='relative group flex items-center mb-3 last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-gray-200 bg-white hover:bg-gray-50 shadow-xs text-xs text-gray-700 cursor-pointer'
|
||||
>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-gray-500 font-semibold'
|
||||
title={branchName.toLocaleUpperCase()}
|
||||
>
|
||||
<div className='inline-block px-0.5 rounded-[5px] bg-white truncate'>{branchName.toLocaleUpperCase()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<BlockIcon
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
className='shrink-0 mr-1.5'
|
||||
/>
|
||||
<div className='grow'>{data.title}</div>
|
||||
{
|
||||
!nodesReadOnly && (
|
||||
<BlockSelector
|
||||
onSelect={handleSelect}
|
||||
placement='top-end'
|
||||
offset={{
|
||||
mainAxis: 6,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={intersection(availablePrevNodes, availableNextNodes)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Item)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { memo } from 'react'
|
||||
|
||||
type LineProps = {
|
||||
linesNumber: number
|
||||
}
|
||||
const Line = ({
|
||||
linesNumber,
|
||||
}: LineProps) => {
|
||||
const svgHeight = linesNumber * 36 + (linesNumber - 1) * 12
|
||||
|
||||
return (
|
||||
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
|
||||
{
|
||||
Array(linesNumber).fill(0).map((_, index) => (
|
||||
<g key={index}>
|
||||
{
|
||||
index === 0 && (
|
||||
<>
|
||||
<rect
|
||||
x={0}
|
||||
y={16}
|
||||
width={1}
|
||||
height={4}
|
||||
fill='#98A2B3'
|
||||
/>
|
||||
<path
|
||||
d='M0,18 L24,18'
|
||||
strokeWidth={1}
|
||||
stroke='#D0D5DD'
|
||||
fill='none'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
index > 0 && (
|
||||
<path
|
||||
d={`M0,18 Q12,18 12,28 L12,${index * 48 + 18 - 10} Q12,${index * 48 + 18} 24,${index * 48 + 18}`}
|
||||
strokeWidth={1}
|
||||
stroke='#D0D5DD'
|
||||
fill='none'
|
||||
/>
|
||||
)
|
||||
}
|
||||
<rect
|
||||
x={23}
|
||||
y={index * 48 + 18 - 2}
|
||||
width={1}
|
||||
height={4}
|
||||
fill='#98A2B3'
|
||||
/>
|
||||
</g>
|
||||
))
|
||||
}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Line)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesInteractions,
|
||||
useNodesSyncDraft,
|
||||
} from '../../../hooks'
|
||||
import type { Node } from '../../../types'
|
||||
import { canRunBySingle } from '../../../utils'
|
||||
import PanelOperator from './panel-operator'
|
||||
import {
|
||||
Play,
|
||||
Stop,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type NodeControlProps = Pick<Node, 'id' | 'data'>
|
||||
const NodeControl: FC<NodeControlProps> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
hidden group-hover:flex pb-1 absolute right-0 -top-7 h-7
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className='flex items-center px-0.5 h-6 bg-white rounded-lg border-[0.5px] border-gray-100 shadow-xs text-gray-500'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{
|
||||
canRunBySingle(data.type) && (
|
||||
<div
|
||||
className='flex items-center justify-center w-5 h-5 rounded-md cursor-pointer hover:bg-black/5'
|
||||
onClick={() => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
_isSingleRun: !data._isSingleRun,
|
||||
},
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
if (!data._isSingleRun)
|
||||
handleSyncWorkflowDraft(true)
|
||||
}}
|
||||
>
|
||||
{
|
||||
data._isSingleRun
|
||||
? <Stop className='w-3 h-3' />
|
||||
: (
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
>
|
||||
<Play className='w-3 h-3' />
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<PanelOperator
|
||||
id={id}
|
||||
data={data}
|
||||
offset={0}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerClassName='!w-5 !h-5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeControl)
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import type { Node } from '../../../types'
|
||||
import BlockSelector from '../../../block-selector'
|
||||
import type { ToolDefaultValue } from '../../../block-selector/types'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
} from '../../../hooks'
|
||||
import { useStore } from '../../../store'
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId: string
|
||||
handleClassName?: string
|
||||
nodeSelectorClassName?: string
|
||||
} & Pick<Node, 'id' | 'data'>
|
||||
|
||||
export const NodeTargetHandle = memo(({
|
||||
id,
|
||||
data,
|
||||
handleId,
|
||||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
}: NodeHandleProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
|
||||
const isConnectable = !!availablePrevNodes.length
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!connected)
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: id,
|
||||
nextNodeTargetHandle: handleId,
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
className={`
|
||||
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500
|
||||
hover:scale-125 transition-all
|
||||
${!connected && 'after:opacity-0'}
|
||||
${data.type === BlockEnum.Start && 'opacity-0'}
|
||||
${handleClassName}
|
||||
`}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !data._isInvalidConnection && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
placement='left'
|
||||
triggerClassName={open => `
|
||||
hidden absolute left-0 top-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availablePrevNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
)
|
||||
})
|
||||
NodeTargetHandle.displayName = 'NodeTargetHandle'
|
||||
|
||||
export const NodeSourceHandle = memo(({
|
||||
id,
|
||||
data,
|
||||
handleId,
|
||||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
}: NodeHandleProps) => {
|
||||
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
|
||||
const isConnectable = !!availableNextNodes.length
|
||||
const connected = data._connectedSourceHandleIds?.includes(handleId)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!connected)
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: id,
|
||||
prevNodeSourceHandle: handleId,
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (notInitialWorkflow && data.type === BlockEnum.Start)
|
||||
setOpen(true)
|
||||
}, [notInitialWorkflow, data.type])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
className={`
|
||||
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
|
||||
hover:scale-125 transition-all
|
||||
${!connected && 'after:opacity-0'}
|
||||
${handleClassName}
|
||||
`}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !data._isInvalidConnection && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden absolute top-0 left-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availableNextNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
)
|
||||
})
|
||||
NodeSourceHandle.displayName = 'NodeSourceHandle'
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title?: string
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const OutputVars: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isFold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(true)
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={toggleFold}
|
||||
className={cn(className, 'flex justify-between leading-[18px] text-[13px] font-semibold text-gray-700 uppercase cursor-pointer')}>
|
||||
<div>{title || t('workflow.nodes.common.outputVars')}</div>
|
||||
<ChevronRight className='w-4 h-4 text-gray-500 transform transition-transform' style={{ transform: isFold ? 'rotate(0deg)' : 'rotate(90deg)' }} />
|
||||
</div>
|
||||
{!isFold && (
|
||||
<div className='mt-2 space-y-1'>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
type VarItemProps = {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
subItems?: {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const VarItem: FC<VarItemProps> = ({
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
subItems,
|
||||
}) => {
|
||||
return (
|
||||
<div className='py-1'>
|
||||
<div className='flex leading-[18px] items-center'>
|
||||
<div className='text-[13px] font-medium text-gray-900 font-mono'>{name}</div>
|
||||
<div className='ml-2 text-xs font-normal text-gray-500 capitalize'>{type}</div>
|
||||
</div>
|
||||
<div className='mt-0.5 leading-[18px] text-xs font-normal text-gray-600'>
|
||||
{description}
|
||||
{subItems && (
|
||||
<div className='ml-2 border-l border-gray-200 pl-2'>
|
||||
{subItems.map((item, index) => (
|
||||
<VarItem
|
||||
key={index}
|
||||
name={item.name}
|
||||
type={item.type}
|
||||
description={item.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(OutputVars)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { intersection } from 'lodash-es'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ChangeBlockProps = {
|
||||
nodeId: string
|
||||
nodeType: BlockEnum
|
||||
sourceHandle: string
|
||||
}
|
||||
const ChangeBlock = ({
|
||||
nodeId,
|
||||
nodeType,
|
||||
sourceHandle,
|
||||
}: ChangeBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availablePrevNodes = nodesExtraData[nodeType].availablePrevNodes
|
||||
const availableNextNodes = nodesExtraData[nodeType].availableNextNodes
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availableNextNodes.length && availableNextNodes.length)
|
||||
return intersection(availablePrevNodes, availableNextNodes)
|
||||
else if (availablePrevNodes.length)
|
||||
return availablePrevNodes
|
||||
else
|
||||
return availableNextNodes
|
||||
}, [availablePrevNodes, availableNextNodes])
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
}, [handleNodeChange, nodeId, sourceHandle])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<div className='flex items-center px-3 w-[232px] h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'>
|
||||
{t('workflow.panel.changeBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: -36,
|
||||
crossAxis: 4,
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='min-w-[240px]'
|
||||
availableBlocksTypes={availableNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ChangeBlock)
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import ChangeBlock from './change-block'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
type PanelOperatorProps = {
|
||||
id: string
|
||||
data: Node['data']
|
||||
triggerClassName?: string
|
||||
offset?: OffsetOptions
|
||||
onOpenChange?: (open: boolean) => void
|
||||
inNode?: boolean
|
||||
}
|
||||
const PanelOperator = ({
|
||||
id,
|
||||
data,
|
||||
triggerClassName,
|
||||
offset = {
|
||||
mainAxis: 4,
|
||||
crossAxis: 53,
|
||||
},
|
||||
onOpenChange,
|
||||
inNode,
|
||||
}: PanelOperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const edges = useEdges()
|
||||
const { handleNodeDelete } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const [open, setOpen] = useState(false)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}, [data, nodesExtraData, buildInTools, customTools])
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools])
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
|
||||
if (onOpenChange)
|
||||
onOpenChange(newOpen)
|
||||
}, [onOpenChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
offset={offset}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => handleOpenChange(!open)}>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-6 h-6 rounded-md cursor-pointer
|
||||
hover:bg-black/5
|
||||
${open && 'bg-black/5'}
|
||||
${triggerClassName}
|
||||
`}
|
||||
>
|
||||
<DotsHorizontal className={`w-4 h-4 ${inNode ? 'text-gray-500' : 'text-gray-700'}`} />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[240px] border-[0.5px] border-gray-200 rounded-lg shadow-xl bg-white'>
|
||||
<div className='p-1'>
|
||||
{
|
||||
data.type !== BlockEnum.Start && !nodesReadOnly && (
|
||||
<ChangeBlock
|
||||
nodeId={id}
|
||||
nodeType={data.type}
|
||||
sourceHandle={edge?.sourceHandle || 'source'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<a
|
||||
href={
|
||||
language === 'zh_Hans'
|
||||
? 'https://docs.dify.ai/v/zh-hans/guides/workflow'
|
||||
: 'https://docs.dify.ai/features/workflow'
|
||||
}
|
||||
target='_blank'
|
||||
className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
>
|
||||
{t('workflow.panel.helpLink')}
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
data.type !== BlockEnum.Start && !nodesReadOnly && (
|
||||
<>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer
|
||||
hover:bg-rose-50 hover:text-red-500
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div className='px-3 py-2 text-xs text-gray-500'>
|
||||
<div className='flex items-center mb-1 h-[22px] font-medium'>
|
||||
{t('workflow.panel.about').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-1 text-gray-700 leading-[18px]'>{about}</div>
|
||||
<div className='leading-[18px]'>
|
||||
{t('workflow.panel.createdBy')} {author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelOperator)
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
BlockEnum,
|
||||
type Node,
|
||||
type NodeOutPutVar,
|
||||
} from '../../../../types'
|
||||
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
import PromptEditorHeightResizeWrap from '@/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import s from '@/app/components/app/configuration/config-prompt/style.module.css'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
|
||||
type Props = {
|
||||
instanceId?: string
|
||||
title: string | JSX.Element
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
readOnly?: boolean
|
||||
showRemove?: boolean
|
||||
onRemove?: () => void
|
||||
justVar?: boolean
|
||||
isChatModel?: boolean
|
||||
isChatApp?: boolean
|
||||
isShowContext?: boolean
|
||||
hasSetBlockStatus?: {
|
||||
context: boolean
|
||||
history: boolean
|
||||
query: boolean
|
||||
}
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
instanceId,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
readOnly,
|
||||
showRemove,
|
||||
onRemove,
|
||||
justVar,
|
||||
isChatModel,
|
||||
isChatApp,
|
||||
isShowContext,
|
||||
hasSetBlockStatus,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const isShowHistory = !isChatModel && isChatApp
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
wrapClassName,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
editorExpandHeight,
|
||||
} = useToggleExpend({ ref })
|
||||
const minHeight = 98
|
||||
const [editorHeight, setEditorHeight] = React.useState(minHeight)
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, [value])
|
||||
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean(false)
|
||||
const hideTooltipRunId = useRef(0)
|
||||
|
||||
const [isShowInsertToolTip, setIsShowInsertTooltip] = useState(false)
|
||||
useEffect(() => {
|
||||
if (isFocus) {
|
||||
clearTimeout(hideTooltipRunId.current)
|
||||
setIsShowInsertTooltip(true)
|
||||
}
|
||||
else {
|
||||
hideTooltipRunId.current = setTimeout(() => {
|
||||
setIsShowInsertTooltip(false)
|
||||
}, 100) as any
|
||||
}
|
||||
}, [isFocus])
|
||||
|
||||
const handleInsertVariable = () => {
|
||||
setFocus()
|
||||
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId } as any)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(wrapClassName)}>
|
||||
<div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}>
|
||||
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
|
||||
<div className='pt-1 pl-3 pr-2 flex justify-between h-6 items-center'>
|
||||
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>
|
||||
<div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
|
||||
{/* Operations */}
|
||||
<div className='flex items-center space-x-2'>
|
||||
{showRemove && (
|
||||
<Trash03 className='w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={onRemove} />
|
||||
)}
|
||||
{!isCopied
|
||||
? (
|
||||
<Clipboard className='w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleCopy} />
|
||||
)
|
||||
: (
|
||||
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-gray-500' />
|
||||
)
|
||||
}
|
||||
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<PromptEditorHeightResizeWrap
|
||||
className={cn(isExpand && 'h-0 grow', 'px-3 min-h-[102px] overflow-y-auto text-sm text-gray-700')}
|
||||
height={isExpand ? editorExpandHeight : editorHeight}
|
||||
minHeight={minHeight}
|
||||
onHeightChange={setEditorHeight}
|
||||
footer={(
|
||||
<div className='pl-3 pb-2 flex'>
|
||||
{(isFocus || isShowInsertToolTip)
|
||||
? (
|
||||
<TooltipPlus
|
||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||
>
|
||||
<div
|
||||
className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500"
|
||||
onClick={handleInsertVariable}
|
||||
>{'{x} '}{t('workflow.nodes.common.insertVarTip')}</div>
|
||||
</TooltipPlus>)
|
||||
: <div className='h-[18px]'></div>}
|
||||
</div>
|
||||
)}
|
||||
hideResize={isExpand}
|
||||
>
|
||||
<>
|
||||
<PromptEditor
|
||||
instanceId={instanceId}
|
||||
className={cn('min-h-[84px]')}
|
||||
compact
|
||||
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: justVar ? false : isShowContext,
|
||||
selectable: !hasSetBlockStatus?.context,
|
||||
canNotAddContext: true,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: justVar ? false : isShowHistory,
|
||||
selectable: !hasSetBlockStatus?.history,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false, // use [sys.query] instead of query block
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
</>
|
||||
</PromptEditorHeightResizeWrap>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Editor)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useWorkflow } from '../../../hooks'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import { VarBlockIcon } from '../../../block-icon'
|
||||
import { getNodeInfoById, isSystemVar } from './variable/utils'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const VAR_PLACEHOLDER = '@#!@#!'
|
||||
|
||||
const ReadonlyInputWithSelectVar: FC<Props> = ({
|
||||
nodeId,
|
||||
value,
|
||||
}) => {
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranch(nodeId)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const res = (() => {
|
||||
const vars: string[] = []
|
||||
const strWithVarPlaceholder = value.replaceAll(/{{#([^#]*)#}}/g, (_match, p1) => {
|
||||
vars.push(p1)
|
||||
return VAR_PLACEHOLDER
|
||||
})
|
||||
|
||||
const html: JSX.Element[] = strWithVarPlaceholder.split(VAR_PLACEHOLDER).map((str, index) => {
|
||||
if (!vars[index])
|
||||
return <span className='relative top-[-3px] leading-[16px]' key={index}>{str}</span>
|
||||
|
||||
const value = vars[index].split('.')
|
||||
const isSystem = isSystemVar(value)
|
||||
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
||||
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
|
||||
|
||||
return (<span key={index}>
|
||||
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
||||
<div className=' inline-flex h-[16px] items-center px-1.5 rounded-[5px] bg-white'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[60px] mx-0.5 text-xs font-medium text-gray-700 truncate' title={node?.title}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[50px] ml-0.5 text-xs font-medium truncate' title={varName}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>)
|
||||
})
|
||||
return html
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className='break-all text-xs'>
|
||||
{res}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ReadonlyInputWithSelectVar)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
const Remove: FC<Props> = ({
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Trash03 className='w-4 h-4' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Remove)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
const i18nPrefix = 'workflow.common.effectVarConfirm'
|
||||
|
||||
const RemoveVarConfirm: FC<Props> = ({
|
||||
isShow,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Confirm
|
||||
isShow={isShow}
|
||||
title={t(`${i18nPrefix}.title`)}
|
||||
content={t(`${i18nPrefix}.content`)}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(RemoveVarConfirm)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
type Item = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
type Props = {
|
||||
trigger?: JSX.Element
|
||||
DropDownIcon?: any
|
||||
noLeft?: boolean
|
||||
options: Item[]
|
||||
value: string
|
||||
placeholder?: string
|
||||
onChange: (value: any) => void
|
||||
uppercase?: boolean
|
||||
popupClassName?: string
|
||||
triggerClassName?: string
|
||||
itemClassName?: string
|
||||
readonly?: boolean
|
||||
showChecked?: boolean
|
||||
}
|
||||
|
||||
const TypeSelector: FC<Props> = ({
|
||||
trigger,
|
||||
DropDownIcon = ChevronSelectorVertical,
|
||||
noLeft,
|
||||
options: list,
|
||||
value,
|
||||
placeholder = '',
|
||||
onChange,
|
||||
uppercase,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
itemClassName,
|
||||
readonly,
|
||||
showChecked,
|
||||
}) => {
|
||||
const noValue = value === '' || value === undefined || value === null
|
||||
const item = list.find(item => item.value === value)
|
||||
const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
|
||||
const ref = React.useRef(null)
|
||||
useClickAway(() => {
|
||||
setHide()
|
||||
}, ref)
|
||||
return (
|
||||
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative')} ref={ref}>
|
||||
{trigger
|
||||
? (
|
||||
<div
|
||||
onClick={toggleShow}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
onClick={toggleShow}
|
||||
className={cn(showOption && 'bg-black/5', 'flex items-center h-5 pl-1 pr-0.5 rounded-md text-xs font-semibold text-gray-700 cursor-pointer hover:bg-black/5')}>
|
||||
<div className={cn(triggerClassName, 'text-sm font-semibold', uppercase && 'uppercase', noValue && 'text-gray-400')}>{!noValue ? item?.label : placeholder}</div>
|
||||
{!readonly && <DropDownIcon className='w-3 h-3 ' />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(showOption && !readonly) && (
|
||||
<div className={cn(popupClassName, 'absolute z-10 top-[24px] w-[120px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white')}>
|
||||
{list.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
setHide()
|
||||
onChange(item.value)
|
||||
}}
|
||||
className={cn(itemClassName, uppercase && 'uppercase', 'flex items-center h-[30px] justify-between min-w-[44px] px-3 rounded-lg cursor-pointer text-[13px] font-medium text-gray-700 hover:bg-gray-50')}
|
||||
>
|
||||
<div>{item.label}</div>
|
||||
{showChecked && item.value === value && <Check className='text-primary-600 w-4 h-4' />}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TypeSelector)
|
||||
18
web/app/components/workflow/nodes/_base/components/split.tsx
Normal file
18
web/app/components/workflow/nodes/_base/components/split.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Split: FC<Props> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'h-[0.5px] bg-black/5')}>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Split)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
|
||||
type Props = {
|
||||
isFocus?: boolean
|
||||
onFocus?: () => void
|
||||
value: string
|
||||
children?: React.ReactNode
|
||||
wrapClassName?: string
|
||||
textClassName?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const SupportVarInput: FC<Props> = ({
|
||||
isFocus,
|
||||
onFocus,
|
||||
children,
|
||||
value,
|
||||
wrapClassName,
|
||||
textClassName,
|
||||
readonly,
|
||||
}) => {
|
||||
const withHightContent = (value || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\{\{([^}]+)\}\}/g, varHighlightHTML({ name: '$1', className: '!mb-0' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
cn(wrapClassName, 'flex w-full h-full')
|
||||
} onClick={onFocus}
|
||||
>
|
||||
{(isFocus && !readonly && children)
|
||||
? (
|
||||
children
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className={cn(textClassName, 'w-0 grow h-full whitespace-nowrap truncate')}
|
||||
title={value}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: withHightContent,
|
||||
}}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SupportVarInput)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import Textarea from 'rc-textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type TitleInputProps = {
|
||||
value: string
|
||||
onBlur: (value: string) => void
|
||||
}
|
||||
|
||||
export const TitleInput = memo(({
|
||||
value,
|
||||
onBlur,
|
||||
}: TitleInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
|
||||
const handleBlur = () => {
|
||||
if (!localValue) {
|
||||
setLocalValue(value)
|
||||
onBlur(value)
|
||||
return
|
||||
}
|
||||
|
||||
onBlur(localValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
value={localValue}
|
||||
onChange={e => setLocalValue(e.target.value)}
|
||||
className={`
|
||||
grow mr-2 px-1 h-6 text-base text-gray-900 font-semibold rounded-lg border border-transparent appearance-none outline-none
|
||||
hover:bg-gray-50
|
||||
focus:border-gray-300 focus:shadow-xs focus:bg-white caret-[#295EFF]
|
||||
`}
|
||||
placeholder={t('workflow.common.addTitle') || ''}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)
|
||||
})
|
||||
TitleInput.displayName = 'TitleInput'
|
||||
|
||||
type DescriptionInputProps = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
export const DescriptionInput = memo(({
|
||||
value,
|
||||
onChange,
|
||||
}: DescriptionInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [focus, setFocus] = useState(false)
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocus(true)
|
||||
}, [])
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocus(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group flex px-2 py-[5px] max-h-[60px] rounded-lg overflow-y-auto
|
||||
border border-transparent hover:bg-gray-50 leading-0
|
||||
${focus && '!border-gray-300 shadow-xs !bg-gray-50'}
|
||||
`}
|
||||
>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
rows={1}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
className={`
|
||||
w-full text-xs text-gray-900 leading-[18px] bg-transparent
|
||||
appearance-none outline-none resize-none
|
||||
placeholder:text-gray-400 caret-[#295EFF]
|
||||
`}
|
||||
placeholder={t('workflow.common.addDescription') || ''}
|
||||
autoSize
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
DescriptionInput.displayName = 'DescriptionInput'
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Expand04 } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import { Collapse04 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type Props = {
|
||||
isExpand: boolean
|
||||
onExpandChange: (isExpand: boolean) => void
|
||||
}
|
||||
|
||||
const ExpandBtn: FC<Props> = ({
|
||||
isExpand,
|
||||
onExpandChange,
|
||||
}) => {
|
||||
const handleToggle = useCallback(() => {
|
||||
onExpandChange(!isExpand)
|
||||
}, [isExpand])
|
||||
|
||||
const Icon = isExpand ? Collapse04 : Expand04
|
||||
return (
|
||||
<Icon className='w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleToggle} />
|
||||
)
|
||||
}
|
||||
export default React.memo(ExpandBtn)
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OutputVar } from '../../../code/types'
|
||||
import RemoveButton from '../remove-button'
|
||||
import VarTypePicker from './var-type-picker'
|
||||
import type { VarType } from '@/app/components/workflow/types'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
outputs: OutputVar
|
||||
outputKeyOrders: string[]
|
||||
onChange: (payload: OutputVar, changedIndex?: number, newKey?: string) => void
|
||||
onRemove: (index: number) => void
|
||||
}
|
||||
|
||||
const OutputVarList: FC<Props> = ({
|
||||
readonly,
|
||||
outputs,
|
||||
outputKeyOrders,
|
||||
onChange,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const list = outputKeyOrders.map((key) => {
|
||||
return {
|
||||
variable: key,
|
||||
variable_type: outputs[key]?.type,
|
||||
}
|
||||
})
|
||||
const handleVarNameChange = useCallback((index: number) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const oldKey = list[index].variable
|
||||
const newKey = e.target.value
|
||||
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (list.map(item => item.variable?.trim()).includes(newKey.trim())) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const newOutputs = produce(outputs, (draft) => {
|
||||
draft[newKey] = draft[oldKey]
|
||||
delete draft[oldKey]
|
||||
})
|
||||
onChange(newOutputs, index, newKey)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, onChange, outputs, outputKeyOrders])
|
||||
|
||||
const handleVarTypeChange = useCallback((index: number) => {
|
||||
return (value: string) => {
|
||||
const key = list[index].variable
|
||||
const newOutputs = produce(outputs, (draft) => {
|
||||
draft[key].type = value as VarType
|
||||
})
|
||||
onChange(newOutputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, onChange, outputs, outputKeyOrders])
|
||||
|
||||
const handleVarRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
onRemove(index)
|
||||
}
|
||||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{list.map((item, index) => (
|
||||
<div className='flex items-center space-x-1' key={index}>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
value={item.variable}
|
||||
onChange={handleVarNameChange(index)}
|
||||
className='w-0 grow h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text' />
|
||||
<VarTypePicker
|
||||
readonly={readonly}
|
||||
value={item.variable_type}
|
||||
onChange={handleVarTypeChange(index)}
|
||||
/>
|
||||
<RemoveButton
|
||||
className='!p-2 !bg-gray-100 hover:!bg-gray-200'
|
||||
onClick={handleVarRemove(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(OutputVarList)
|
||||
|
|
@ -0,0 +1,542 @@
|
|||
import produce from 'immer'
|
||||
import { isArray, uniq } from 'lodash-es'
|
||||
import type { CodeNodeType } from '../../../code/types'
|
||||
import type { EndNodeType } from '../../../end/types'
|
||||
import type { AnswerNodeType } from '../../../answer/types'
|
||||
import type { LLMNodeType } from '../../../llm/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
|
||||
import type { IfElseNodeType } from '../../../if-else/types'
|
||||
import type { TemplateTransformNodeType } from '../../../template-transform/types'
|
||||
import type { QuestionClassifierNodeType } from '../../../question-classifier/types'
|
||||
import type { HttpNodeType } from '../../../http/types'
|
||||
import { VarType as ToolVarType } from '../../../tool/types'
|
||||
import type { ToolNodeType } from '../../../tool/types'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import {
|
||||
CHAT_QUESTION_CLASSIFIER_OUTPUT_STRUCT,
|
||||
COMPLETION_QUESTION_CLASSIFIER_OUTPUT_STRUCT,
|
||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
|
||||
LLM_OUTPUT_STRUCT,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
|
||||
TOOL_OUTPUT_STRUCT,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import type { PromptItem } from '@/models/debug'
|
||||
import { VAR_REGEX } from '@/config'
|
||||
|
||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
if (type === InputVarType.number)
|
||||
return VarType.number
|
||||
|
||||
return VarType.string
|
||||
}
|
||||
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector): Var => {
|
||||
const { children } = obj
|
||||
const res: Var = {
|
||||
variable: obj.variable,
|
||||
type: VarType.object,
|
||||
children: children.filter((item: Var) => {
|
||||
const { children } = item
|
||||
const currSelector = [...value_selector, item.variable]
|
||||
if (!children)
|
||||
return filterVar(item, currSelector)
|
||||
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector)
|
||||
return obj.children && obj.children?.length > 0
|
||||
}),
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, selector: ValueSelector) => boolean): NodeOutPutVar => {
|
||||
const { id, data } = item
|
||||
const res: NodeOutPutVar = {
|
||||
nodeId: id,
|
||||
title: data.title,
|
||||
vars: [],
|
||||
}
|
||||
switch (data.type) {
|
||||
case BlockEnum.Start: {
|
||||
const {
|
||||
variables,
|
||||
} = data as StartNodeType
|
||||
res.vars = variables.map((v) => {
|
||||
return {
|
||||
variable: v.variable,
|
||||
type: inputVarTypeToVarType(v.type),
|
||||
isParagraph: v.type === InputVarType.paragraph,
|
||||
isSelect: v.type === InputVarType.select,
|
||||
options: v.options,
|
||||
required: v.required,
|
||||
}
|
||||
})
|
||||
if (isChatMode) {
|
||||
res.vars.push({
|
||||
variable: 'sys.query',
|
||||
type: VarType.string,
|
||||
})
|
||||
}
|
||||
res.vars.push({
|
||||
variable: 'sys.files',
|
||||
type: VarType.arrayFile,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
res.vars = LLM_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Code: {
|
||||
const {
|
||||
outputs,
|
||||
} = data as CodeNodeType
|
||||
res.vars = Object.keys(outputs).map((key) => {
|
||||
return {
|
||||
variable: key,
|
||||
type: outputs[key].type,
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.TemplateTransform: {
|
||||
res.vars = TEMPLATE_TRANSFORM_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.QuestionClassifier: {
|
||||
res.vars = isChatMode ? CHAT_QUESTION_CLASSIFIER_OUTPUT_STRUCT : COMPLETION_QUESTION_CLASSIFIER_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.HttpRequest: {
|
||||
res.vars = HTTP_REQUEST_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAssigner: {
|
||||
const {
|
||||
output_type,
|
||||
} = data as VariableAssignerNodeType
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'output',
|
||||
type: output_type,
|
||||
},
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Tool: {
|
||||
res.vars = TOOL_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const selector = [id]
|
||||
res.vars = res.vars.filter((v) => {
|
||||
const { children } = v
|
||||
if (!children)
|
||||
return filterVar(v, selector)
|
||||
|
||||
const obj = findExceptVarInObject(v, filterVar, selector)
|
||||
return obj?.children && obj?.children.length > 0
|
||||
}).map((v) => {
|
||||
const { children } = v
|
||||
if (!children)
|
||||
return v
|
||||
|
||||
return findExceptVarInObject(v, filterVar, selector)
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
export const toNodeOutputVars = (nodes: any[], isChatMode: boolean, filterVar = (_payload: Var, _selector: ValueSelector) => true): NodeOutPutVar[] => {
|
||||
const res = nodes
|
||||
.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type))
|
||||
.map((node) => {
|
||||
return {
|
||||
...formatItem(node, isChatMode, filterVar),
|
||||
isStartNode: node.data.type === BlockEnum.Start,
|
||||
}
|
||||
})
|
||||
.filter(item => item.vars.length > 0)
|
||||
return res
|
||||
}
|
||||
|
||||
export const isSystemVar = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
|
||||
}
|
||||
|
||||
export const getNodeInfoById = (nodes: any, id: string) => {
|
||||
if (!isArray(nodes))
|
||||
return
|
||||
return nodes.find((node: any) => node.id === id)
|
||||
}
|
||||
|
||||
export const getVarType = (value: ValueSelector, availableNodes: any[], isChatMode: boolean): VarType | undefined => {
|
||||
const isSystem = isSystemVar(value)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
|
||||
const targetVarNodeId = isSystem ? startNode?.id : value[0]
|
||||
const targetVar = allOutputVars.find(v => v.nodeId === targetVarNodeId)
|
||||
|
||||
if (!targetVar)
|
||||
return undefined
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
return curr.find((v: any) => v.variable === (value as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
(value as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
type = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const matchNotSystemVars = (prompts: string[]) => {
|
||||
if (!prompts)
|
||||
return []
|
||||
|
||||
const allVars: string[] = []
|
||||
prompts.forEach((prompt) => {
|
||||
VAR_REGEX.lastIndex = 0
|
||||
allVars.push(...(prompt.match(VAR_REGEX) || []))
|
||||
})
|
||||
const uniqVars = uniq(allVars).map(v => v.replaceAll('{{#', '').replace('#}}', '').split('.'))
|
||||
return uniqVars
|
||||
}
|
||||
|
||||
export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
const { data } = node
|
||||
const { type } = data
|
||||
let res: ValueSelector[] = []
|
||||
switch (type) {
|
||||
case BlockEnum.End: {
|
||||
res = (data as EndNodeType).outputs?.map((output) => {
|
||||
return output.value_selector
|
||||
})
|
||||
break
|
||||
}
|
||||
case BlockEnum.Answer: {
|
||||
res = (data as AnswerNodeType).variables?.map((v) => {
|
||||
return v.value_selector
|
||||
})
|
||||
break
|
||||
}
|
||||
case BlockEnum.LLM: {
|
||||
const payload = (data as LLMNodeType)
|
||||
const isChatModel = payload.model?.mode === 'chat'
|
||||
let prompts: string[] = []
|
||||
if (isChatModel)
|
||||
prompts = (payload.prompt_template as PromptItem[])?.map(p => p.text) || []
|
||||
else
|
||||
prompts = [(payload.prompt_template as PromptItem).text]
|
||||
|
||||
const inputVars: ValueSelector[] = matchNotSystemVars(prompts)
|
||||
const contextVar = (data as LLMNodeType).context?.variable_selector ? [(data as LLMNodeType).context?.variable_selector] : []
|
||||
res = [...inputVars, ...contextVar]
|
||||
break
|
||||
}
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res = [(data as KnowledgeRetrievalNodeType).query_variable_selector]
|
||||
break
|
||||
}
|
||||
case BlockEnum.IfElse: {
|
||||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector
|
||||
})
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
res = (data as CodeNodeType).variables?.map((v) => {
|
||||
return v.value_selector
|
||||
})
|
||||
break
|
||||
}
|
||||
case BlockEnum.TemplateTransform: {
|
||||
res = (data as TemplateTransformNodeType).variables?.map((v: any) => {
|
||||
return v.value_selector
|
||||
})
|
||||
break
|
||||
}
|
||||
case BlockEnum.QuestionClassifier: {
|
||||
res = [(data as QuestionClassifierNodeType).query_variable_selector]
|
||||
break
|
||||
}
|
||||
case BlockEnum.HttpRequest: {
|
||||
const payload = (data as HttpNodeType)
|
||||
res = matchNotSystemVars([payload.url, payload.headers, payload.params, payload.body.data])
|
||||
break
|
||||
}
|
||||
case BlockEnum.Tool: {
|
||||
const payload = (data as ToolNodeType)
|
||||
const mixVars = matchNotSystemVars(Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type === ToolVarType.mixed).map(key => payload.tool_parameters[key].value) as string[])
|
||||
const vars = Object.keys(payload.tool_parameters).filter(key => payload.tool_parameters[key].type === ToolVarType.variable).map(key => payload.tool_parameters[key].value as string) || []
|
||||
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAssigner: {
|
||||
res = (data as VariableAssignerNodeType)?.variables
|
||||
}
|
||||
}
|
||||
return res || []
|
||||
}
|
||||
|
||||
export const findUsedVarNodes = (varSelector: ValueSelector, availableNodes: Node[]): Node[] => {
|
||||
const res: Node[] = []
|
||||
availableNodes.forEach((node) => {
|
||||
const vars = getNodeUsedVars(node)
|
||||
if (vars.find(v => v.join('.') === varSelector.join('.')))
|
||||
res.push(node)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, newVarSelector: ValueSelector): Node => {
|
||||
const newNode = produce(oldNode, (draft: any) => {
|
||||
const { data } = draft
|
||||
const { type } = data
|
||||
switch (type) {
|
||||
case BlockEnum.End: {
|
||||
const payload = data as EndNodeType
|
||||
if (payload.outputs) {
|
||||
payload.outputs = payload.outputs.map((output) => {
|
||||
if (output.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
output.value_selector = newVarSelector
|
||||
return output
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Answer: {
|
||||
const payload = data as AnswerNodeType
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v) => {
|
||||
if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
v.value_selector = newVarSelector
|
||||
return v
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.LLM: {
|
||||
const payload = data as LLMNodeType
|
||||
// TODO: update in inputs
|
||||
// if (payload.variables) {
|
||||
// payload.variables = payload.variables.map((v) => {
|
||||
// if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
// v.value_selector = newVarSelector
|
||||
// return v
|
||||
// })
|
||||
// }
|
||||
if (payload.context?.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
payload.context.variable_selector = newVarSelector
|
||||
|
||||
break
|
||||
}
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
const payload = data as KnowledgeRetrievalNodeType
|
||||
if (payload.query_variable_selector.join('.') === oldVarSelector.join('.'))
|
||||
payload.query_variable_selector = newVarSelector
|
||||
break
|
||||
}
|
||||
case BlockEnum.IfElse: {
|
||||
const payload = data as IfElseNodeType
|
||||
if (payload.conditions) {
|
||||
payload.conditions = payload.conditions.map((c) => {
|
||||
if (c.variable_selector.join('.') === oldVarSelector.join('.'))
|
||||
c.variable_selector = newVarSelector
|
||||
return c
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
const payload = data as CodeNodeType
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v) => {
|
||||
if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
v.value_selector = newVarSelector
|
||||
return v
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.TemplateTransform: {
|
||||
const payload = data as TemplateTransformNodeType
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v: any) => {
|
||||
if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
v.value_selector = newVarSelector
|
||||
return v
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.QuestionClassifier: {
|
||||
const payload = data as QuestionClassifierNodeType
|
||||
if (payload.query_variable_selector.join('.') === oldVarSelector.join('.'))
|
||||
payload.query_variable_selector = newVarSelector
|
||||
break
|
||||
}
|
||||
case BlockEnum.HttpRequest: {
|
||||
// TODO: update in inputs
|
||||
// const payload = data as HttpNodeType
|
||||
// if (payload.variables) {
|
||||
// payload.variables = payload.variables.map((v) => {
|
||||
// if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
// v.value_selector = newVarSelector
|
||||
// return v
|
||||
// })
|
||||
// }
|
||||
break
|
||||
}
|
||||
case BlockEnum.Tool: {
|
||||
// TODO: update in inputs
|
||||
// const payload = data as ToolNodeType
|
||||
// if (payload.tool_parameters) {
|
||||
// payload.tool_parameters = payload.tool_parameters.map((v) => {
|
||||
// if (v.type === VarKindType.static)
|
||||
// return v
|
||||
|
||||
// if (v.value_selector?.join('.') === oldVarSelector.join('.'))
|
||||
// v.value_selector = newVarSelector
|
||||
// return v
|
||||
// })
|
||||
// }
|
||||
break
|
||||
}
|
||||
case BlockEnum.VariableAssigner: {
|
||||
const payload = data as VariableAssignerNodeType
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v) => {
|
||||
if (v.join('.') === oldVarSelector.join('.'))
|
||||
v = newVarSelector
|
||||
return v
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
return newNode
|
||||
}
|
||||
const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
if (!v.variable)
|
||||
return
|
||||
|
||||
res.push([...parentValueSelector, v.variable])
|
||||
|
||||
if (v.children && v.children.length > 0) {
|
||||
v.children.forEach((child) => {
|
||||
varToValueSelectorList(child, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
if (Array.isArray(vars)) {
|
||||
vars.forEach((v) => {
|
||||
varToValueSelectorList(v, parentValueSelector, res)
|
||||
})
|
||||
}
|
||||
varToValueSelectorList(vars as Var, parentValueSelector, res)
|
||||
}
|
||||
|
||||
export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelector[] => {
|
||||
const { data, id } = node
|
||||
const { type } = data
|
||||
let res: ValueSelector[] = []
|
||||
|
||||
switch (type) {
|
||||
case BlockEnum.Start: {
|
||||
const {
|
||||
variables,
|
||||
} = data as StartNodeType
|
||||
res = variables.map((v) => {
|
||||
return [id, v.variable]
|
||||
})
|
||||
|
||||
if (isChatMode) {
|
||||
res.push([id, 'sys', 'query'])
|
||||
res.push([id, 'sys', 'files'])
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
varsToValueSelectorList(KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Code: {
|
||||
const {
|
||||
outputs,
|
||||
} = data as CodeNodeType
|
||||
Object.keys(outputs).forEach((key) => {
|
||||
res.push([id, key])
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.TemplateTransform: {
|
||||
varsToValueSelectorList(TEMPLATE_TRANSFORM_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.QuestionClassifier: {
|
||||
varsToValueSelectorList(isChatMode ? CHAT_QUESTION_CLASSIFIER_OUTPUT_STRUCT : COMPLETION_QUESTION_CLASSIFIER_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.HttpRequest: {
|
||||
varsToValueSelectorList(HTTP_REQUEST_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAssigner: {
|
||||
res.push([id, 'output'])
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Tool: {
|
||||
varsToValueSelectorList(TOOL_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import RemoveButton from '../remove-button'
|
||||
import VarReferencePicker from './var-reference-picker'
|
||||
import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
list: Variable[]
|
||||
onChange: (list: Variable[]) => void
|
||||
isSupportConstantValue?: boolean
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
}
|
||||
|
||||
const VarList: FC<Props> = ({
|
||||
nodeId,
|
||||
readonly,
|
||||
list,
|
||||
onChange,
|
||||
isSupportConstantValue,
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleVarNameChange = useCallback((index: number) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index].variable = e.target.value
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleVarReferenceChange = useCallback((index: number) => {
|
||||
return (value: ValueSelector | string, varKindType: VarKindType) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
if (!isSupportConstantValue || varKindType === VarKindType.variable) {
|
||||
draft[index].value_selector = value as ValueSelector
|
||||
if (isSupportConstantValue)
|
||||
draft[index].variable_type = VarKindType.variable
|
||||
|
||||
if (!draft[index].variable)
|
||||
draft[index].variable = value[value.length - 1]
|
||||
}
|
||||
else {
|
||||
draft[index].variable_type = VarKindType.constant
|
||||
draft[index].value_selector = value as ValueSelector
|
||||
draft[index].value = value as string
|
||||
}
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [isSupportConstantValue, list, onChange])
|
||||
|
||||
const handleVarRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{list.map((item, index) => (
|
||||
<div className='flex items-center space-x-1' key={index}>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
value={list[index].variable}
|
||||
onChange={handleVarNameChange(index)}
|
||||
placeholder={t('workflow.common.variableNamePlaceholder')!}
|
||||
className='w-[120px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text'
|
||||
/>
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
readonly={readonly}
|
||||
isShowNodeName
|
||||
className='grow'
|
||||
value={item.variable_type === VarKindType.constant ? (item.value || '') : (item.value_selector || [])}
|
||||
isSupportConstantValue={isSupportConstantValue}
|
||||
onChange={handleVarReferenceChange(index)}
|
||||
defaultVarKindType={item.variable_type}
|
||||
onlyLeafNodeVar={onlyLeafNodeVar}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
{!readonly && (
|
||||
<RemoveButton
|
||||
className='!p-2 !bg-gray-100 hover:!bg-gray-200'
|
||||
onClick={handleVarRemove(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarList)
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isSystemVar, toNodeOutputVars } from './utils'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
nodeId: string
|
||||
isShowNodeName: boolean
|
||||
readonly: boolean
|
||||
value: ValueSelector | string
|
||||
onChange: (value: ValueSelector | string, varKindType: VarKindType) => void
|
||||
onOpen?: () => void
|
||||
isSupportConstantValue?: boolean
|
||||
defaultVarKindType?: VarKindType
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
}
|
||||
|
||||
const VarReferencePicker: FC<Props> = ({
|
||||
nodeId,
|
||||
readonly,
|
||||
className,
|
||||
isShowNodeName,
|
||||
value,
|
||||
onOpen = () => { },
|
||||
onChange,
|
||||
isSupportConstantValue,
|
||||
defaultVarKindType = VarKindType.constant,
|
||||
onlyLeafNodeVar,
|
||||
filterVar = () => true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState(TRIGGER_DEFAULT_WIDTH)
|
||||
useEffect(() => {
|
||||
if (triggerRef.current)
|
||||
setTriggerWidth(triggerRef.current.clientWidth)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [triggerRef.current])
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
const outputVars = toNodeOutputVars(availableNodes, isChatMode, filterVar)
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
const hasValue = !isConstant && value.length > 0
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
const outputVarNodeId = hasValue ? value[0] : ''
|
||||
const outputVarNode = (() => {
|
||||
if (!hasValue || isConstant)
|
||||
return null
|
||||
if (isSystemVar(value as ValueSelector))
|
||||
return startNode?.data
|
||||
|
||||
return getNodeInfoById(availableNodes, outputVarNodeId)?.data
|
||||
})()
|
||||
const varName = hasValue ? `${isSystemVar(value as ValueSelector) ? 'sys.' : ''}${value[value.length - 1]}` : ''
|
||||
|
||||
const getVarType = () => {
|
||||
if (isConstant)
|
||||
return 'undefined'
|
||||
const isSystem = isSystemVar(value as ValueSelector)
|
||||
const targetVarNodeId = isSystem ? startNode?.id : outputVarNodeId
|
||||
const targetVar = allOutputVars.find(v => v.nodeId === targetVarNodeId)
|
||||
|
||||
if (!targetVar)
|
||||
return 'undefined'
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
return curr.find((v: any) => v.variable === (value as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
(value as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
type = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const varKindTypes = [
|
||||
{
|
||||
label: 'Variable',
|
||||
value: VarKindType.variable,
|
||||
},
|
||||
{
|
||||
label: 'Constant',
|
||||
value: VarKindType.constant,
|
||||
},
|
||||
]
|
||||
|
||||
const handleVarKindTypeChange = useCallback((value: VarKindType) => {
|
||||
setVarKindType(value)
|
||||
if (value === VarKindType.constant)
|
||||
onChange('', value)
|
||||
else
|
||||
onChange([], value)
|
||||
}, [onChange])
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const [controlFocus, setControlFocus] = useState(0)
|
||||
useEffect(() => {
|
||||
if (controlFocus && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
setIsFocus(true)
|
||||
}
|
||||
}, [controlFocus])
|
||||
|
||||
const handleVarReferenceChange = useCallback((value: ValueSelector) => {
|
||||
// sys var not passed to backend
|
||||
const newValue = produce(value, (draft) => {
|
||||
if (draft[1] && draft[1].startsWith('sys')) {
|
||||
draft.shift()
|
||||
const paths = draft[0].split('.')
|
||||
paths.forEach((p, i) => {
|
||||
draft[i] = p
|
||||
})
|
||||
}
|
||||
})
|
||||
onChange(newValue, varKindType)
|
||||
setOpen(false)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
const handleStaticChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value as string, varKindType)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
const handleClearVar = useCallback(() => {
|
||||
if (varKindType === VarKindType.constant)
|
||||
onChange('', varKindType)
|
||||
else
|
||||
onChange([], varKindType)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
const type = getVarType()
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const availableWidth = triggerWidth - 56
|
||||
const [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth] = (() => {
|
||||
const totalTextLength = ((outputVarNode?.title || '') + (varName || '') + (type || '')).length
|
||||
const PRIORITY_WIDTH = 15
|
||||
const maxNodeNameWidth = PRIORITY_WIDTH + Math.floor((outputVarNode?.title?.length || 0) / totalTextLength * availableWidth)
|
||||
const maxVarNameWidth = -PRIORITY_WIDTH + Math.floor((varName?.length || 0) / totalTextLength * availableWidth)
|
||||
const maxTypeWidth = Math.floor((type?.length || 0) / totalTextLength * availableWidth)
|
||||
return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth]
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
}} className='!flex'>
|
||||
<div ref={triggerRef} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8 p-1 rounded-lg bg-gray-100 border')}>
|
||||
{isSupportConstantValue
|
||||
? <div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setControlFocus(Date.now())
|
||||
}} className='mr-1 flex items-center space-x-1'>
|
||||
<TypeSelector
|
||||
noLeft
|
||||
triggerClassName='!text-xs'
|
||||
readonly={readonly}
|
||||
DropDownIcon={ChevronDown}
|
||||
value={varKindType}
|
||||
options={varKindTypes}
|
||||
onChange={handleVarKindTypeChange}
|
||||
/>
|
||||
<div className='h-4 w-px bg-black/5'></div>
|
||||
</div>
|
||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>)}
|
||||
{isConstant
|
||||
? (
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 pl-0.5 bg-transparent text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
|
||||
value={isConstant ? value : ''}
|
||||
onChange={handleStaticChange}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
<div className='ml-0.5 text-xs font-medium truncate' title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
</div>
|
||||
)}
|
||||
{(hasValue && !readonly) && (<div
|
||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={handleClearVar}
|
||||
>
|
||||
<XClose className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||
</div>)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
{!isConstant && (
|
||||
<VarReferencePopup
|
||||
vars={outputVars}
|
||||
onChange={handleVarReferenceChange}
|
||||
itemWidth={triggerWidth}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(VarReferencePicker)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
import { type NodeOutPutVar, type ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
vars: NodeOutPutVar[]
|
||||
onChange: (value: ValueSelector) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
vars,
|
||||
onChange,
|
||||
itemWidth,
|
||||
}) => {
|
||||
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
|
||||
return (
|
||||
<div className='p-1 bg-white rounded-lg border border-gray-200 shadow-lg space-y-1' style={{
|
||||
width: itemWidth || 228,
|
||||
}}>
|
||||
<VarReferenceVars
|
||||
searchBoxClassName='mt-1'
|
||||
vars={vars}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth} />
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(VarReferencePopup)
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean, useHover } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type NodeOutPutVar, type ValueSelector, type Var, VarType } from '@/app/components/workflow/types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import {
|
||||
SearchLg,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
title: string
|
||||
data: Var[]
|
||||
objPath: string[]
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
onHovering?: (value: boolean) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
title: string
|
||||
objPath: string[]
|
||||
itemData: Var
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
onHovering?: (value: boolean) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
nodeId,
|
||||
title,
|
||||
objPath,
|
||||
itemData,
|
||||
onChange,
|
||||
onHovering,
|
||||
itemWidth,
|
||||
}) => {
|
||||
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
|
||||
const itemRef = useRef(null)
|
||||
const [isItemHovering, setIsItemHovering] = useState(false)
|
||||
const _ = useHover(itemRef, {
|
||||
onChange: (hovering) => {
|
||||
if (hovering) {
|
||||
setIsItemHovering(true)
|
||||
}
|
||||
else {
|
||||
if (isObj) {
|
||||
setTimeout(() => {
|
||||
setIsItemHovering(false)
|
||||
}, 100)
|
||||
}
|
||||
else {
|
||||
setIsItemHovering(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const [isChildrenHovering, setIsChildrenHovering] = useState(false)
|
||||
const isHovering = isItemHovering || isChildrenHovering
|
||||
const open = isObj && isHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (itemData.variable.startsWith('sys.')) { // system variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
else {
|
||||
onChange([nodeId, ...objPath, itemData.variable], itemData)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={() => { }}
|
||||
placement='left-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger className='w-full'>
|
||||
<div
|
||||
ref={itemRef}
|
||||
className={cn(
|
||||
isObj ? ' pr-1' : 'pr-[18px]',
|
||||
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
|
||||
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
|
||||
}
|
||||
// style={{ width: itemWidth || 252 }}
|
||||
onClick={handleChosen}
|
||||
>
|
||||
<div className='flex items-center w-0 grow'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable}</div>
|
||||
</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
|
||||
{isObj && (
|
||||
<ChevronRight className='ml-0.5 w-3 h-3 text-gray-500' />
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
{isObj && (
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
<ObjectChildren
|
||||
nodeId={nodeId}
|
||||
title={title}
|
||||
objPath={[...objPath, itemData.variable]}
|
||||
data={itemData.children as Var[]}
|
||||
onChange={onChange}
|
||||
onHovering={setIsChildrenHovering}
|
||||
itemWidth={itemWidth}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
const ObjectChildren: FC<ObjectChildrenProps> = ({
|
||||
title,
|
||||
nodeId,
|
||||
objPath,
|
||||
data,
|
||||
onChange,
|
||||
onHovering,
|
||||
itemWidth,
|
||||
}) => {
|
||||
const currObjPath = objPath
|
||||
const itemRef = useRef(null)
|
||||
const [isItemHovering, setIsItemHovering] = useState(false)
|
||||
const _ = useHover(itemRef, {
|
||||
onChange: (hovering) => {
|
||||
if (hovering) {
|
||||
setIsItemHovering(true)
|
||||
}
|
||||
else {
|
||||
setTimeout(() => {
|
||||
setIsItemHovering(false)
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
})
|
||||
const [isChildrenHovering, setIsChildrenHovering] = useState(false)
|
||||
const isHovering = isItemHovering || isChildrenHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isHovering])
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isItemHovering)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isItemHovering])
|
||||
// absolute top-[-2px]
|
||||
return (
|
||||
<div ref={itemRef} className=' bg-white rounded-lg border border-gray-200 shadow-lg space-y-1' style={{
|
||||
right: itemWidth ? itemWidth - 10 : 215,
|
||||
minWidth: 252,
|
||||
}}>
|
||||
<div className='flex items-center h-[22px] px-3 text-xs font-normal text-gray-700'><span className='text-gray-500'>{title}.</span>{currObjPath.join('.')}</div>
|
||||
{
|
||||
(data && data.length > 0)
|
||||
&& data.map((v, i) => (
|
||||
<Item
|
||||
key={i}
|
||||
nodeId={nodeId}
|
||||
title={title}
|
||||
objPath={objPath}
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
onHovering={setIsChildrenHovering}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
hideSearch?: boolean
|
||||
searchBoxClassName?: string
|
||||
vars: NodeOutPutVar[]
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
hideSearch,
|
||||
searchBoxClassName,
|
||||
vars,
|
||||
onChange,
|
||||
itemWidth,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const filteredVars = vars.filter((v) => {
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.'))
|
||||
return children.length > 0
|
||||
}).filter((v) => {
|
||||
if (!searchText)
|
||||
return v
|
||||
const children = v.vars.filter(v => v.variable.toLowerCase().includes(searchText.toLowerCase()))
|
||||
return children.length > 0
|
||||
}).map((v) => {
|
||||
let vars = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.'))
|
||||
if (searchText)
|
||||
vars = vars.filter(v => v.variable.toLowerCase().includes(searchText.toLowerCase()))
|
||||
|
||||
return {
|
||||
...v,
|
||||
vars,
|
||||
}
|
||||
})
|
||||
const [isFocus, {
|
||||
setFalse: setBlur,
|
||||
setTrue: setFocus,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!hideSearch && (
|
||||
<>
|
||||
<div
|
||||
className={cn(searchBoxClassName, isFocus && 'shadow-sm bg-white', 'mb-2 mx-1 flex items-center px-2 rounded-lg bg-gray-100 ')}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
|
||||
<SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
|
||||
<input
|
||||
value={searchText}
|
||||
className='grow px-0.5 py-[7px] text-[13px] text-gray-700 bg-transparent appearance-none outline-none caret-primary-600 placeholder:text-gray-400'
|
||||
placeholder={t('workflow.common.searchVar') || ''}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onFocus={setFocus}
|
||||
onBlur={setBlur}
|
||||
autoFocus
|
||||
/>
|
||||
{
|
||||
searchText && (
|
||||
<div
|
||||
className='flex items-center justify-center ml-[5px] w-[18px] h-[18px] cursor-pointer'
|
||||
onClick={() => setSearchText('')}
|
||||
>
|
||||
<XCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='h-[0.5px] bg-black/5 relative left-[-4px]' style={{
|
||||
width: 'calc(100% + 8px)',
|
||||
}}></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{filteredVars.length > 0
|
||||
? <div>
|
||||
|
||||
{
|
||||
filteredVars.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div
|
||||
className='leading-[22px] px-3 text-xs font-medium text-gray-500 uppercase truncate'
|
||||
title={item.title}
|
||||
>{item.title}</div>
|
||||
{item.vars.map((v, j) => (
|
||||
<Item
|
||||
key={j}
|
||||
title={item.title}
|
||||
nodeId={item.nodeId}
|
||||
objPath={[]}
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
}
|
||||
</div>
|
||||
: <div className='pl-3 leading-[18px] text-xs font-medium text-gray-500 uppercase'>{t('workflow.common.noVar')}</div>}
|
||||
</ >
|
||||
)
|
||||
}
|
||||
export default React.memo(VarReferenceVars)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
readonly: boolean
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const TYPES = [VarType.string, VarType.number, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.object]
|
||||
const VarReferencePicker: FC<Props> = ({
|
||||
readonly,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleChange = useCallback((type: string) => {
|
||||
return () => {
|
||||
setOpen(false)
|
||||
onChange(type)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer select-none')}>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='w-[120px] cursor-pointer'>
|
||||
<div className='flex items-center h-8 justify-between px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px]'>
|
||||
<div className='capitalize grow w-0 truncate' title={value}>{value}</div>
|
||||
<ChevronDown className='shrink-0 w-3.5 h-3.5 text-gray-700' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
<div className='w-[120px] p-1 bg-white rounded-lg shadow-sm'>
|
||||
{TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
className='flex items-center h-[30px] justify-between pl-3 pr-2 rounded-lg hover:bg-gray-100 text-gray-900 text-[13px] cursor-pointer'
|
||||
onClick={handleChange(type)}
|
||||
>
|
||||
<div className='w-0 grow capitalize truncate'>{type}</div>
|
||||
{type === value && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarReferencePicker)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
}
|
||||
const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
}: Params = {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
const availableVars = toNodeOutputVars(availableNodes, isChatMode, filterVar)
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
}
|
||||
}
|
||||
|
||||
export default useAvailableVarList
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const setInputs = (newInputs: CommonNodeType<T>) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: newInputs,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
inputs: data,
|
||||
setInputs,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNodeCrud
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { unionBy } from 'lodash-es'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { getNodeInfoById, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { singleNodeRun } from '@/service/workflow'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
import IfElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||
import CodeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
import TemplateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
|
||||
import QuestionClassifyDefault from '@/app/components/workflow/nodes/question-classifier/default'
|
||||
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
|
||||
import ToolDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
import VariableAssigner from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
|
||||
const { checkValid: checkLLMValid } = LLMDefault
|
||||
const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault
|
||||
const { checkValid: checkIfElseValid } = IfElseDefault
|
||||
const { checkValid: checkCodeValid } = CodeDefault
|
||||
const { checkValid: checkTemplateTransformValid } = TemplateTransformDefault
|
||||
const { checkValid: checkQuestionClassifyValid } = QuestionClassifyDefault
|
||||
const { checkValid: checkHttpValid } = HTTPDefault
|
||||
const { checkValid: checkToolValid } = ToolDefault
|
||||
const { checkValid: checkVariableAssignerValid } = VariableAssigner
|
||||
|
||||
const checkValidFns: Record<BlockEnum, Function> = {
|
||||
[BlockEnum.LLM]: checkLLMValid,
|
||||
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
|
||||
[BlockEnum.IfElse]: checkIfElseValid,
|
||||
[BlockEnum.Code]: checkCodeValid,
|
||||
[BlockEnum.TemplateTransform]: checkTemplateTransformValid,
|
||||
[BlockEnum.QuestionClassifier]: checkQuestionClassifyValid,
|
||||
[BlockEnum.HttpRequest]: checkHttpValid,
|
||||
[BlockEnum.Tool]: checkToolValid,
|
||||
[BlockEnum.VariableAssigner]: checkVariableAssignerValid,
|
||||
} as any
|
||||
|
||||
type Params<T> = {
|
||||
id: string
|
||||
data: CommonNodeType<T>
|
||||
defaultRunInputData: Record<string, any>
|
||||
moreDataForCheckValid?: any
|
||||
}
|
||||
|
||||
const varTypeToInputVarType = (type: VarType, {
|
||||
isSelect,
|
||||
isParagraph,
|
||||
}: {
|
||||
isSelect: boolean
|
||||
isParagraph: boolean
|
||||
}) => {
|
||||
if (isSelect)
|
||||
return InputVarType.select
|
||||
if (isParagraph)
|
||||
return InputVarType.paragraph
|
||||
if (type === VarType.number)
|
||||
return InputVarType.number
|
||||
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
|
||||
return InputVarType.json
|
||||
if (type === VarType.arrayFile)
|
||||
return InputVarType.files
|
||||
|
||||
return InputVarType.textInput
|
||||
}
|
||||
|
||||
const useOneStepRun = <T>({
|
||||
id,
|
||||
data,
|
||||
defaultRunInputData,
|
||||
moreDataForCheckValid,
|
||||
}: Params<T>) => {
|
||||
const { t } = useTranslation()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow() as any
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const allOutputVars = toNodeOutputVars(getBeforeNodesInSameBranch(id), isChatMode)
|
||||
const getVar = (valueSelector: ValueSelector): Var | undefined => {
|
||||
let res: Var | undefined
|
||||
const isSystem = valueSelector[0] === 'sys'
|
||||
const targetVar = isSystem ? allOutputVars.find(item => !!item.isStartNode) : allOutputVars.find(v => v.nodeId === valueSelector[0])
|
||||
if (!targetVar)
|
||||
return undefined
|
||||
if (isSystem)
|
||||
return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1])
|
||||
|
||||
let curr: any = targetVar.vars
|
||||
valueSelector.slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
res = curr
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const checkValid = checkValidFns[data.type]
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
|
||||
const [runResult, setRunResult] = useState<any>(null)
|
||||
|
||||
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
|
||||
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
|
||||
const isShowSingleRun = data._isSingleRun && canShowSingleRun
|
||||
useEffect(() => {
|
||||
if (!checkValid) {
|
||||
setCanShowSingleRun(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (data._isSingleRun) {
|
||||
const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid)
|
||||
setCanShowSingleRun(isValid)
|
||||
if (!isValid) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
},
|
||||
})
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data._isSingleRun])
|
||||
const hideSingleRun = () => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
|
||||
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
|
||||
|
||||
const handleRun = async (submitData: Record<string, any>) => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Running,
|
||||
},
|
||||
})
|
||||
let res: any
|
||||
try {
|
||||
res = await singleNodeRun(appId!, id, { inputs: submitData }) as any
|
||||
if (res.error)
|
||||
throw new Error(res.error)
|
||||
}
|
||||
catch (e: any) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
setRunResult({
|
||||
...res,
|
||||
created_by: res.created_by_account?.name || '',
|
||||
})
|
||||
}
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.NotStart,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const toVarInputs = (variables: Variable[]): InputVar[] => {
|
||||
if (!variables)
|
||||
return []
|
||||
|
||||
const varInputs = variables.map((item) => {
|
||||
const originalVar = getVar(item.value_selector)
|
||||
if (!originalVar) {
|
||||
return {
|
||||
label: item.label || item.variable,
|
||||
variable: item.variable,
|
||||
type: InputVarType.textInput,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: item.label || item.variable,
|
||||
variable: item.variable,
|
||||
type: varTypeToInputVarType(originalVar.type, {
|
||||
isSelect: !!originalVar.isSelect,
|
||||
isParagraph: !!originalVar.isParagraph,
|
||||
}),
|
||||
required: item.required !== false,
|
||||
options: originalVar.options,
|
||||
}
|
||||
})
|
||||
|
||||
return varInputs
|
||||
}
|
||||
|
||||
const getInputVars = (textList: string[]) => {
|
||||
const valueSelectors: ValueSelector[] = []
|
||||
textList.forEach((text) => {
|
||||
valueSelectors.push(...doGetInputVars(text))
|
||||
})
|
||||
|
||||
const variables = unionBy(valueSelectors, item => item.join('.')).map((item) => {
|
||||
const varInfo = getNodeInfoById(availableNodes, item[0])?.data
|
||||
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.type,
|
||||
nodeName: varInfo?.title || availableNodes[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `#${item.join('.')}#`,
|
||||
value_selector: item,
|
||||
}
|
||||
})
|
||||
|
||||
const varInputs = toVarInputs(variables)
|
||||
return varInputs
|
||||
}
|
||||
|
||||
return {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
toVarInputs,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
isCompleted,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useOneStepRun
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { type OutputVar } from '../../code/types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
type Params<T> = {
|
||||
id: string
|
||||
inputs: T
|
||||
setInputs: (newInputs: T) => void
|
||||
varKey?: string
|
||||
outputKeyOrders: string[]
|
||||
onOutputKeyOrdersChange: (newOutputKeyOrders: string[]) => void
|
||||
}
|
||||
function useOutputVarList<T>({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
varKey = 'outputs',
|
||||
outputKeyOrders = [],
|
||||
onOutputKeyOrdersChange,
|
||||
}: Params<T>) {
|
||||
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
|
||||
|
||||
const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
draft[varKey] = newVars
|
||||
})
|
||||
setInputs(newInputs)
|
||||
|
||||
if (changedIndex !== undefined) {
|
||||
const newOutputKeyOrders = produce(outputKeyOrders, (draft) => {
|
||||
draft[changedIndex] = newKey!
|
||||
})
|
||||
onOutputKeyOrdersChange(newOutputKeyOrders)
|
||||
}
|
||||
|
||||
if (newKey)
|
||||
handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey])
|
||||
}, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange])
|
||||
|
||||
const handleAddVariable = useCallback(() => {
|
||||
const newKey = `var_${Object.keys((inputs as any)[varKey]).length + 1}`
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
draft[varKey] = {
|
||||
...draft[varKey],
|
||||
[newKey]: {
|
||||
type: VarType.string,
|
||||
children: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
onOutputKeyOrdersChange([...outputKeyOrders, newKey])
|
||||
}, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange])
|
||||
|
||||
const [isShowRemoveVarConfirm, {
|
||||
setTrue: showRemoveVarConfirm,
|
||||
setFalse: hideRemoveVarConfirm,
|
||||
}] = useBoolean(false)
|
||||
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
|
||||
const removeVarInNode = useCallback(() => {
|
||||
removeUsedVarInNodes(removedVar)
|
||||
hideRemoveVarConfirm()
|
||||
}, [hideRemoveVarConfirm, removeUsedVarInNodes, removedVar])
|
||||
const handleRemoveVariable = useCallback((index: number) => {
|
||||
const key = outputKeyOrders[index]
|
||||
|
||||
if (isVarUsedInNodes([id, key])) {
|
||||
showRemoveVarConfirm()
|
||||
setRemovedVar([id, key])
|
||||
return
|
||||
}
|
||||
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
delete draft[varKey][key]
|
||||
})
|
||||
setInputs(newInputs)
|
||||
onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))
|
||||
}, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, showRemoveVarConfirm, varKey])
|
||||
|
||||
return {
|
||||
handleVarsChange,
|
||||
handleAddVariable,
|
||||
handleRemoveVariable,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm: removeVarInNode,
|
||||
}
|
||||
}
|
||||
|
||||
export default useOutputVarList
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export type UseResizePanelPrarams = {
|
||||
direction?: 'horizontal' | 'vertical' | 'both'
|
||||
triggerDirection?: 'top' | 'right' | 'bottom' | 'left' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
||||
minWidth?: number
|
||||
maxWidth?: number
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
onResized?: (width: number, height: number) => void
|
||||
}
|
||||
export const useResizePanel = (params?: UseResizePanelPrarams) => {
|
||||
const {
|
||||
direction = 'both',
|
||||
triggerDirection = 'bottom-right',
|
||||
minWidth = -Infinity,
|
||||
maxWidth = Infinity,
|
||||
minHeight = -Infinity,
|
||||
maxHeight = Infinity,
|
||||
onResized,
|
||||
} = params || {}
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const initXRef = useRef(0)
|
||||
const initYRef = useRef(0)
|
||||
const initContainerWidthRef = useRef(0)
|
||||
const initContainerHeightRef = useRef(0)
|
||||
const isResizingRef = useRef(false)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
|
||||
|
||||
const handleStartResize = useCallback((e: MouseEvent) => {
|
||||
initXRef.current = e.clientX
|
||||
initYRef.current = e.clientY
|
||||
initContainerWidthRef.current = containerRef.current?.offsetWidth || minWidth
|
||||
initContainerHeightRef.current = containerRef.current?.offsetHeight || minHeight
|
||||
isResizingRef.current = true
|
||||
setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
|
||||
document.body.style.userSelect = 'none'
|
||||
}, [minWidth, minHeight])
|
||||
|
||||
const handleResize = useCallback((e: MouseEvent) => {
|
||||
if (!isResizingRef.current)
|
||||
return
|
||||
|
||||
if (!containerRef.current)
|
||||
return
|
||||
|
||||
if (direction === 'horizontal' || direction === 'both') {
|
||||
const offsetX = e.clientX - initXRef.current
|
||||
let width = 0
|
||||
if (triggerDirection === 'left' || triggerDirection === 'top-left' || triggerDirection === 'bottom-left')
|
||||
width = initContainerWidthRef.current - offsetX
|
||||
else if (triggerDirection === 'right' || triggerDirection === 'top-right' || triggerDirection === 'bottom-right')
|
||||
width = initContainerWidthRef.current + offsetX
|
||||
|
||||
if (width < minWidth)
|
||||
width = minWidth
|
||||
if (width > maxWidth)
|
||||
width = maxWidth
|
||||
containerRef.current.style.width = `${width}px`
|
||||
}
|
||||
|
||||
if (direction === 'vertical' || direction === 'both') {
|
||||
const offsetY = e.clientY - initYRef.current
|
||||
let height = 0
|
||||
if (triggerDirection === 'top' || triggerDirection === 'top-left' || triggerDirection === 'top-right')
|
||||
height = initContainerHeightRef.current - offsetY
|
||||
else if (triggerDirection === 'bottom' || triggerDirection === 'bottom-left' || triggerDirection === 'bottom-right')
|
||||
height = initContainerHeightRef.current + offsetY
|
||||
|
||||
if (height < minHeight)
|
||||
height = minHeight
|
||||
if (height > maxHeight)
|
||||
height = maxHeight
|
||||
|
||||
containerRef.current.style.height = `${height}px`
|
||||
}
|
||||
}, [
|
||||
direction,
|
||||
triggerDirection,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
])
|
||||
|
||||
const handleStopResize = useCallback(() => {
|
||||
isResizingRef.current = false
|
||||
document.body.style.userSelect = prevUserSelectStyle
|
||||
|
||||
if (onResized && containerRef.current)
|
||||
onResized(containerRef.current.offsetWidth, containerRef.current.offsetHeight)
|
||||
}, [prevUserSelectStyle, onResized])
|
||||
|
||||
useEffect(() => {
|
||||
const element = triggerRef.current
|
||||
element?.addEventListener('mousedown', handleStartResize)
|
||||
document.addEventListener('mousemove', handleResize)
|
||||
document.addEventListener('mouseup', handleStopResize)
|
||||
return () => {
|
||||
if (element)
|
||||
element.removeEventListener('mousedown', handleStartResize)
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
}
|
||||
}, [handleStartResize, handleResize, handleStopResize])
|
||||
|
||||
return {
|
||||
triggerRef,
|
||||
containerRef,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
type Params = {
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
hasFooter?: boolean
|
||||
}
|
||||
|
||||
const useToggleExpend = ({ ref, hasFooter = true }: Params) => {
|
||||
const [isExpand, setIsExpand] = useState(false)
|
||||
const [wrapHeight, setWrapHeight] = useState(ref.current?.clientHeight)
|
||||
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
|
||||
useEffect(() => {
|
||||
setWrapHeight(ref.current?.clientHeight)
|
||||
}, [isExpand])
|
||||
|
||||
const wrapClassName = isExpand && 'absolute z-10 left-4 right-6 top-[52px] bottom-0 pb-4 bg-white'
|
||||
|
||||
return {
|
||||
wrapClassName,
|
||||
editorExpandHeight,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
}
|
||||
}
|
||||
|
||||
export default useToggleExpend
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Variable } from '@/app/components/workflow/types'
|
||||
|
||||
type Params<T> = {
|
||||
inputs: T
|
||||
setInputs: (newInputs: T) => void
|
||||
varKey?: string
|
||||
}
|
||||
function useVarList<T>({
|
||||
inputs,
|
||||
setInputs,
|
||||
varKey = 'variables',
|
||||
}: Params<T>) {
|
||||
const handleVarListChange = useCallback((newList: Variable[] | string) => {
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
draft[varKey] = newList as Variable[]
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs, varKey])
|
||||
|
||||
const handleAddVariable = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
draft[varKey].push({
|
||||
variable: '',
|
||||
value_selector: [],
|
||||
})
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs, varKey])
|
||||
return {
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
}
|
||||
}
|
||||
|
||||
export default useVarList
|
||||
130
web/app/components/workflow/nodes/_base/node.tsx
Normal file
130
web/app/components/workflow/nodes/_base/node.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import type {
|
||||
FC,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
} from 'react'
|
||||
import type { NodeProps } from '../../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
NodeRunningStatus,
|
||||
} from '../../types'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
} from '../../hooks'
|
||||
import {
|
||||
NodeSourceHandle,
|
||||
NodeTargetHandle,
|
||||
} from './components/node-handle'
|
||||
import NodeControl from './components/node-control'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
CheckCircle,
|
||||
Loading02,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
|
||||
type BaseNodeProps = {
|
||||
children: ReactElement
|
||||
} & NodeProps
|
||||
|
||||
const BaseNode: FC<BaseNodeProps> = ({
|
||||
id,
|
||||
data,
|
||||
children,
|
||||
}) => {
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const toolIcon = useToolIcon(data)
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex border-[2px] rounded-2xl
|
||||
${(data.selected && !data._runningStatus && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group relative pb-1 w-[240px] bg-[#fcfdff] shadow-xs
|
||||
border border-transparent rounded-[15px]
|
||||
${!data._runningStatus && 'hover:shadow-lg'}
|
||||
${data._runningStatus === NodeRunningStatus.Running && '!border-primary-500'}
|
||||
${data._runningStatus === NodeRunningStatus.Succeeded && '!border-[#12B76A]'}
|
||||
${data._runningStatus === NodeRunningStatus.Failed && '!border-[#F04438]'}
|
||||
${data._runningStatus === NodeRunningStatus.Waiting && 'opacity-70'}
|
||||
${data._isInvalidConnection && '!border-[#F04438]'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
data.type !== BlockEnum.VariableAssigner && !data._runningStatus && !nodesReadOnly && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName='!top-4 !-left-[9px] !translate-y-0'
|
||||
handleId='target'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._runningStatus && !nodesReadOnly && (
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName='!top-4 !-right-[9px] !translate-y-0'
|
||||
handleId='source'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!data._runningStatus && !nodesReadOnly && (
|
||||
<NodeControl
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center px-3 pt-3 pb-2'>
|
||||
<BlockIcon
|
||||
className='shrink-0 mr-2'
|
||||
type={data.type}
|
||||
size='md'
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
<div
|
||||
title={data.title}
|
||||
className='grow mr-1 text-[13px] font-semibold text-gray-700 truncate'
|
||||
>
|
||||
{data.title}
|
||||
</div>
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
<Loading02 className='w-3.5 h-3.5 text-primary-600 animate-spin' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Succeeded && (
|
||||
<CheckCircle className='w-3.5 h-3.5 text-[#12B76A]' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Failed && (
|
||||
<AlertCircle className='w-3.5 h-3.5 text-[#F04438]' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{cloneElement(children, { id, data })}
|
||||
{
|
||||
data.desc && (
|
||||
<div className='px-3 pt-1 pb-2 text-xs leading-[18px] text-gray-500 whitespace-pre-line break-words'>
|
||||
{data.desc}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BaseNode)
|
||||
162
web/app/components/workflow/nodes/_base/panel.tsx
Normal file
162
web/app/components/workflow/nodes/_base/panel.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import type {
|
||||
FC,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NextStep from './components/next-step'
|
||||
import PanelOperator from './components/panel-operator'
|
||||
import {
|
||||
DescriptionInput,
|
||||
TitleInput,
|
||||
} from './components/title-description-input'
|
||||
import { useResizePanel } from './hooks/use-resize-panel'
|
||||
import {
|
||||
XClose,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
useToolIcon,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { canRunBySingle } from '@/app/components/workflow/utils'
|
||||
import { Play } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactElement
|
||||
} & Node
|
||||
|
||||
const BasePanel: FC<BasePanelProps> = ({
|
||||
id,
|
||||
data,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const initPanelWidth = localStorage.getItem('workflow-node-panel-width') || 420
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const handleResized = useCallback((width: number) => {
|
||||
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
triggerRef,
|
||||
containerRef,
|
||||
} = useResizePanel({
|
||||
direction: 'horizontal',
|
||||
triggerDirection: 'left',
|
||||
minWidth: 420,
|
||||
maxWidth: 720,
|
||||
onResized: handleResized,
|
||||
})
|
||||
|
||||
const {
|
||||
handleNodeDataUpdate,
|
||||
handleNodeDataUpdateWithSyncDraft,
|
||||
} = useNodeDataUpdate()
|
||||
|
||||
const handleTitleBlur = useCallback((title: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
const handleDescriptionChange = useCallback((desc: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
return (
|
||||
<div className='relative mr-2 h-full'>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute top-1/2 -translate-y-1/2 -left-2 w-3 h-6 cursor-col-resize resize-x'>
|
||||
<div className='w-1 h-6 bg-gray-300 rounded-sm'></div>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='relative h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl overflow-y-auto'
|
||||
style={{
|
||||
width: `${initPanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className='sticky top-0 bg-white border-b-[0.5px] border-black/5 z-10'>
|
||||
<div className='flex items-center px-4 pt-4 pb-1'>
|
||||
<BlockIcon
|
||||
className='shrink-0 mr-1'
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
size='md'
|
||||
/>
|
||||
<TitleInput
|
||||
value={data.title || ''}
|
||||
onBlur={handleTitleBlur}
|
||||
/>
|
||||
<div className='shrink-0 flex items-center text-gray-500'>
|
||||
{
|
||||
canRunBySingle(data.type) && !nodesReadOnly && (
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-center mr-1 w-6 h-6 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={() => {
|
||||
handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
|
||||
handleSyncWorkflowDraft(true)
|
||||
}}
|
||||
>
|
||||
<Play className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
<PanelOperator id={id} data={data} />
|
||||
<div className='mx-3 w-[1px] h-3.5 bg-gray-200' />
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => handleNodeSelect(id, true)}
|
||||
>
|
||||
<XClose className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<DescriptionInput
|
||||
value={data.desc || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
{cloneElement(children, { id, data })}
|
||||
</div>
|
||||
{
|
||||
!!availableNextNodes.length && (
|
||||
<div className='p-4 border-t-[0.5px] border-t-black/5'>
|
||||
<div className='flex items-center mb-1 text-gray-700 text-[13px] font-semibold'>
|
||||
{t('workflow.panel.nextStep').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-2 text-xs text-gray-400'>
|
||||
{t('workflow.panel.addNextStep')}
|
||||
</div>
|
||||
<NextStep selectedNode={{ id, data } as Node} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BasePanel)
|
||||
36
web/app/components/workflow/nodes/answer/default.ts
Normal file
36
web/app/components/workflow/nodes/answer/default.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { AnswerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const nodeDefault: NodeDefault<AnswerNodeType> = {
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
answer: '',
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: AnswerNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { answer } = payload
|
||||
if (!answer)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.answer.answer') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
26
web/app/components/workflow/nodes/answer/node.tsx
Normal file
26
web/app/components/workflow/nodes/answer/node.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfoPanel from '../_base/components/info-panel'
|
||||
import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var'
|
||||
import type { AnswerNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
const Node: FC<NodeProps<AnswerNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<InfoPanel title={t('workflow.nodes.answer.answer')} content={
|
||||
<ReadonlyInputWithSelectVar
|
||||
value={data.answer}
|
||||
nodeId={id}
|
||||
/>
|
||||
} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
44
web/app/components/workflow/nodes/answer/panel.tsx
Normal file
44
web/app/components/workflow/nodes/answer/panel.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useConfig from './use-config'
|
||||
import type { AnswerNodeType } from './types'
|
||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
const i18nPrefix = 'workflow.nodes.answer'
|
||||
|
||||
const Panel: FC<NodePanelProps<AnswerNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleAnswerChange,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const { availableVars, availableNodes } = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mt-2 mb-2 px-4 space-y-4'>
|
||||
<Editor
|
||||
readOnly={readOnly}
|
||||
justVar
|
||||
title={t(`${i18nPrefix}.answer`)!}
|
||||
value={inputs.answer}
|
||||
onChange={handleAnswerChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
6
web/app/components/workflow/nodes/answer/types.ts
Normal file
6
web/app/components/workflow/nodes/answer/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export type AnswerNodeType = CommonNodeType & {
|
||||
variables: Variable[]
|
||||
answer: string
|
||||
}
|
||||
41
web/app/components/workflow/nodes/answer/use-config.ts
Normal file
41
web/app/components/workflow/nodes/answer/use-config.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import type { Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import type { AnswerNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: AnswerNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<AnswerNodeType>(id, payload)
|
||||
// variables
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<AnswerNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const handleAnswerChange = useCallback((value: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.answer = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type !== VarType.arrayObject
|
||||
}, [])
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleAnswerChange,
|
||||
filterVar,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
5
web/app/components/workflow/nodes/answer/utils.ts
Normal file
5
web/app/components/workflow/nodes/answer/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { AnswerNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: AnswerNodeType) => {
|
||||
return true
|
||||
}
|
||||
43
web/app/components/workflow/nodes/code/default.ts
Normal file
43
web/app/components/workflow/nodes/code/default.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { CodeLanguage, type CodeNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<CodeNodeType> = {
|
||||
defaultValue: {
|
||||
code: '',
|
||||
code_language: CodeLanguage.python3,
|
||||
variables: [],
|
||||
outputs: {},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: CodeNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { code, variables } = payload
|
||||
if (!errorMessages && variables.filter(v => !v.variable).length > 0)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
if (!errorMessages && !code)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.code`) })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
13
web/app/components/workflow/nodes/code/node.tsx
Normal file
13
web/app/components/workflow/nodes/code/node.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { CodeNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<CodeNodeType>> = () => {
|
||||
return (
|
||||
// No summary content
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
142
web/app/components/workflow/nodes/code/panel.tsx
Normal file
142
web/app/components/workflow/nodes/code/panel.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
|
||||
import useConfig from './use-config'
|
||||
import type { CodeNodeType } from './types'
|
||||
import { CodeLanguage } from './types'
|
||||
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
|
||||
import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.code'
|
||||
|
||||
const codeLanguages = [
|
||||
{
|
||||
label: 'Python3',
|
||||
value: CodeLanguage.python3,
|
||||
},
|
||||
{
|
||||
label: 'JavaScript',
|
||||
value: CodeLanguage.javascript,
|
||||
},
|
||||
]
|
||||
const Panel: FC<NodePanelProps<CodeNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
outputKeyOrders,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleRemoveVariable,
|
||||
handleCodeChange,
|
||||
handleCodeLanguageChange,
|
||||
handleVarsChange,
|
||||
handleAddOutputVariable,
|
||||
filterVar,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
// single run
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVars`)}
|
||||
operations={
|
||||
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
|
||||
}
|
||||
>
|
||||
<VarList
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
list={inputs.variables}
|
||||
onChange={handleVarListChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<CodeEditor
|
||||
readOnly={readOnly}
|
||||
title={
|
||||
<TypeSelector
|
||||
options={codeLanguages}
|
||||
value={inputs.code_language}
|
||||
onChange={handleCodeLanguageChange}
|
||||
/>
|
||||
}
|
||||
language={inputs.code_language}
|
||||
value={inputs.code}
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.outputVars`)}
|
||||
operations={
|
||||
<AddButton onClick={handleAddOutputVariable} />
|
||||
}
|
||||
>
|
||||
|
||||
<OutputVarList
|
||||
readonly={readOnly}
|
||||
outputs={inputs.outputs}
|
||||
outputKeyOrders={outputKeyOrders}
|
||||
onChange={handleVarsChange}
|
||||
onRemove={handleRemoveVariable}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{
|
||||
isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: varInputs,
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<RemoveEffectVarConfirm
|
||||
isShow={isShowRemoveVarConfirm}
|
||||
onCancel={hideRemoveVarConfirm}
|
||||
onConfirm={onRemoveVarConfirm}
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
19
web/app/components/workflow/nodes/code/types.ts
Normal file
19
web/app/components/workflow/nodes/code/types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { CommonNodeType, VarType, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export enum CodeLanguage {
|
||||
python3 = 'python3',
|
||||
javascript = 'javascript',
|
||||
json = 'json',
|
||||
}
|
||||
|
||||
export type OutputVar = Record<string, {
|
||||
type: VarType
|
||||
children: null // support nest in the future,
|
||||
}>
|
||||
|
||||
export type CodeNodeType = CommonNodeType & {
|
||||
variables: Variable[]
|
||||
code_language: CodeLanguage
|
||||
code: string
|
||||
outputs: OutputVar
|
||||
}
|
||||
169
web/app/components/workflow/nodes/code/use-config.ts
Normal file
169
web/app/components/workflow/nodes/code/use-config.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import useOutputVarList from '../_base/hooks/use-output-var-list'
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import type { Var } from '../../types'
|
||||
import { useStore } from '../../store'
|
||||
import type { CodeNodeType, OutputVar } from './types'
|
||||
import { CodeLanguage } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import { fetchNodeDefault } from '@/service/workflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: CodeNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
|
||||
const [allLanguageDefault, setAllLanguageDefault] = useState<Record<CodeLanguage, CodeNodeType> | null>(null)
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
(async () => {
|
||||
const { config: javaScriptConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.javascript }) as any
|
||||
const { config: pythonConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.python3 }) as any
|
||||
setAllLanguageDefault({
|
||||
[CodeLanguage.javascript]: javaScriptConfig as CodeNodeType,
|
||||
[CodeLanguage.python3]: pythonConfig as CodeNodeType,
|
||||
} as any)
|
||||
})()
|
||||
}
|
||||
}, [appId])
|
||||
|
||||
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
|
||||
const { inputs, setInputs } = useNodeCrud<CodeNodeType>(id, payload)
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<CodeNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>([])
|
||||
const syncOutputKeyOrders = useCallback((outputs: OutputVar) => {
|
||||
setOutputKeyOrders(Object.keys(outputs))
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (inputs.code) {
|
||||
if (inputs.outputs && Object.keys(inputs.outputs).length > 0)
|
||||
syncOutputKeyOrders(inputs.outputs)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
|
||||
if (isReady) {
|
||||
setInputs({
|
||||
...inputs,
|
||||
...defaultConfig,
|
||||
})
|
||||
syncOutputKeyOrders(defaultConfig.outputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultConfig])
|
||||
|
||||
const handleCodeChange = useCallback((code: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.code = code
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleCodeLanguageChange = useCallback((codeLanguage: CodeLanguage) => {
|
||||
const currDefaultConfig = allLanguageDefault?.[codeLanguage]
|
||||
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.code_language = codeLanguage
|
||||
if (!currDefaultConfig)
|
||||
return
|
||||
draft.code = currDefaultConfig.code
|
||||
draft.variables = currDefaultConfig.variables
|
||||
draft.outputs = currDefaultConfig.outputs
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [allLanguageDefault, inputs, setInputs])
|
||||
|
||||
const {
|
||||
handleVarsChange,
|
||||
handleAddVariable: handleAddOutputVariable,
|
||||
handleRemoveVariable,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
} = useOutputVarList<CodeNodeType>({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
outputKeyOrders,
|
||||
onOutputKeyOrdersChange: setOutputKeyOrders,
|
||||
})
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
toVarInputs,
|
||||
runningStatus,
|
||||
isCompleted,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
} = useOneStepRun<CodeNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
defaultRunInputData: {},
|
||||
})
|
||||
|
||||
const varInputs = toVarInputs(inputs.variables)
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
outputKeyOrders,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleRemoveVariable,
|
||||
handleCodeChange,
|
||||
handleCodeLanguageChange,
|
||||
handleVarsChange,
|
||||
filterVar,
|
||||
handleAddOutputVariable,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
// single run
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
isCompleted,
|
||||
handleRun,
|
||||
handleStop,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
runResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
5
web/app/components/workflow/nodes/code/utils.ts
Normal file
5
web/app/components/workflow/nodes/code/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { CodeNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: CodeNodeType) => {
|
||||
return true
|
||||
}
|
||||
56
web/app/components/workflow/nodes/constants.ts
Normal file
56
web/app/components/workflow/nodes/constants.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import type { ComponentType } from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
import EndNode from './end/node'
|
||||
import EndPanel from './end/panel'
|
||||
import AnswerNode from './answer/node'
|
||||
import AnswerPanel from './answer/panel'
|
||||
import LLMNode from './llm/node'
|
||||
import LLMPanel from './llm/panel'
|
||||
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
|
||||
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
|
||||
import QuestionClassifierNode from './question-classifier/node'
|
||||
import QuestionClassifierPanel from './question-classifier/panel'
|
||||
import IfElseNode from './if-else/node'
|
||||
import IfElsePanel from './if-else/panel'
|
||||
import CodeNode from './code/node'
|
||||
import CodePanel from './code/panel'
|
||||
import TemplateTransformNode from './template-transform/node'
|
||||
import TemplateTransformPanel from './template-transform/panel'
|
||||
import HttpNode from './http/node'
|
||||
import HttpPanel from './http/panel'
|
||||
import ToolNode from './tool/node'
|
||||
import ToolPanel from './tool/panel'
|
||||
import VariableAssignerNode from './variable-assigner/node'
|
||||
import VariableAssignerPanel from './variable-assigner/panel'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
[BlockEnum.End]: EndNode,
|
||||
[BlockEnum.Answer]: AnswerNode,
|
||||
[BlockEnum.LLM]: LLMNode,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
|
||||
[BlockEnum.IfElse]: IfElseNode,
|
||||
[BlockEnum.Code]: CodeNode,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformNode,
|
||||
[BlockEnum.HttpRequest]: HttpNode,
|
||||
[BlockEnum.Tool]: ToolNode,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
|
||||
[BlockEnum.IfElse]: IfElsePanel,
|
||||
[BlockEnum.Code]: CodePanel,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
|
||||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
}
|
||||
33
web/app/components/workflow/nodes/end/default.ts
Normal file
33
web/app/components/workflow/nodes/end/default.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { type EndNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const nodeDefault: NodeDefault<EndNodeType> = {
|
||||
defaultValue: {
|
||||
outputs: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes() {
|
||||
return []
|
||||
},
|
||||
checkValid(payload: EndNodeType) {
|
||||
let isValid = true
|
||||
let errorMessages = ''
|
||||
if (payload.type) {
|
||||
isValid = true
|
||||
errorMessages = ''
|
||||
}
|
||||
return {
|
||||
isValid,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
93
web/app/components/workflow/nodes/end/node.tsx
Normal file
93
web/app/components/workflow/nodes/end/node.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { EndNodeType } from './types'
|
||||
import type { NodeProps, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import { isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
const outputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const getNode = (id: string) => {
|
||||
return availableNodes.find(node => node.id === id) || startNode
|
||||
}
|
||||
|
||||
const getVarType = (nodeId: string, value: ValueSelector) => {
|
||||
const targetVar = outputVars.find(v => v.nodeId === nodeId)
|
||||
if (!targetVar)
|
||||
return 'undefined'
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
const isSystem = isSystemVar(value);
|
||||
(value).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
curr = curr.find((v: any) => v.variable === isSystem ? `sys.${key}` : key)
|
||||
if (isLast) {
|
||||
type = curr.type
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
return type
|
||||
}
|
||||
const { outputs } = data
|
||||
const filteredOutputs = (outputs as Variable[]).filter(({ value_selector }) => value_selector.length > 0)
|
||||
|
||||
if (!filteredOutputs.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1 space-y-0.5'>
|
||||
{filteredOutputs.map(({ value_selector }, index) => {
|
||||
const node = getNode(value_selector[0])
|
||||
const isSystem = isSystemVar(value_selector)
|
||||
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
|
||||
return (
|
||||
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||
<div className='flex items-center text-xs font-medium text-gray-500'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[75px] truncate'>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[50px] ml-0.5 text-xs font-medium truncate'>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize'>{getVarType(node?.id || '', value_selector)}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
49
web/app/components/workflow/nodes/end/panel.tsx
Normal file
49
web/app/components/workflow/nodes/end/panel.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { type FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useConfig from './use-config'
|
||||
import type { EndNodeType } from './types'
|
||||
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import { type NodePanelProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.end'
|
||||
|
||||
const Panel: FC<NodePanelProps<EndNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const outputs = inputs.outputs
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.output.variable`)}
|
||||
operations={
|
||||
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
|
||||
}
|
||||
>
|
||||
<VarList
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
list={outputs}
|
||||
onChange={handleVarListChange}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
5
web/app/components/workflow/nodes/end/types.ts
Normal file
5
web/app/components/workflow/nodes/end/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export type EndNodeType = CommonNodeType & {
|
||||
outputs: Variable[]
|
||||
}
|
||||
27
web/app/components/workflow/nodes/end/use-config.ts
Normal file
27
web/app/components/workflow/nodes/end/use-config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import type { EndNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
const useConfig = (id: string, payload: EndNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<EndNodeType>(id, payload)
|
||||
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<EndNodeType>({
|
||||
inputs,
|
||||
setInputs: (newInputs) => {
|
||||
setInputs(newInputs)
|
||||
},
|
||||
varKey: 'outputs',
|
||||
})
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
5
web/app/components/workflow/nodes/end/utils.ts
Normal file
5
web/app/components/workflow/nodes/end/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { EndNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: EndNodeType) => {
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Method } from '../types'
|
||||
import Selector from '../../_base/components/selector'
|
||||
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
|
||||
import { VarType } from '../../../types'
|
||||
import type { Var } from '../../../types'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const MethodOptions = [
|
||||
{ label: 'GET', value: Method.get },
|
||||
{ label: 'POST', value: Method.post },
|
||||
{ label: 'HEAD', value: Method.head },
|
||||
{ label: 'PATCH', value: Method.patch },
|
||||
{ label: 'PUT', value: Method.put },
|
||||
{ label: 'DELETE', value: Method.delete },
|
||||
]
|
||||
type Props = {
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
method: Method
|
||||
onMethodChange: (method: Method) => void
|
||||
url: string
|
||||
onUrlChange: (url: string) => void
|
||||
}
|
||||
|
||||
const ApiInput: FC<Props> = ({
|
||||
nodeId,
|
||||
readonly,
|
||||
method,
|
||||
onMethodChange,
|
||||
url,
|
||||
onUrlChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex items-start space-x-1'>
|
||||
<Selector
|
||||
value={method}
|
||||
onChange={onMethodChange}
|
||||
options={MethodOptions}
|
||||
trigger={
|
||||
<div className={cn(readonly && 'cursor-pointer', 'h-8 shrink-0 flex items-center px-2.5 bg-gray-100 border-black/5 rounded-lg')} >
|
||||
<div className='w-12 pl-0.5 leading-[18px] text-xs font-medium text-gray-900 uppercase'>{method}</div>
|
||||
{!readonly && <ChevronDown className='ml-1 w-3.5 h-3.5 text-gray-700' />}
|
||||
</div>
|
||||
}
|
||||
popupClassName='top-[34px] w-[108px]'
|
||||
showChecked
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
<Input
|
||||
instanceId='http-api-url'
|
||||
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||
value={url}
|
||||
onChange={onUrlChange}
|
||||
readOnly={readonly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={!readonly ? t('workflow.nodes.http.apiPlaceholder')! : ''}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(ApiInput)
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Authorization as AuthorizationPayloadType } from '../../types'
|
||||
import { APIType, AuthorizationType } from '../../types'
|
||||
import RadioGroup from './radio-group'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http.authorization'
|
||||
|
||||
type Props = {
|
||||
payload: AuthorizationPayloadType
|
||||
onChange: (payload: AuthorizationPayloadType) => void
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const Field = ({ title, isRequired, children }: { title: string; isRequired?: boolean; children: JSX.Element }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='leading-8 text-[13px] font-medium text-gray-700'>
|
||||
{title}
|
||||
{isRequired && <span className='ml-0.5 text-[#D92D20]'>*</span>}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Authorization: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
isShow,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [tempPayload, setTempPayload] = React.useState<AuthorizationPayloadType>(payload)
|
||||
const handleAuthTypeChange = useCallback((type: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
draft.type = type as AuthorizationType
|
||||
if (draft.type === AuthorizationType.apiKey && !draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleAuthAPITypeChange = useCallback((type: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config.type = type as APIType
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleAPIKeyOrHeaderChange = useCallback((type: 'api_key' | 'header') => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config[type] = e.target.value
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onChange(tempPayload)
|
||||
onHide()
|
||||
}, [tempPayload, onChange, onHide])
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.authorization`)}
|
||||
wrapperClassName='z-50 w-400'
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
>
|
||||
<div>
|
||||
<div className='space-y-2'>
|
||||
<Field title={t(`${i18nPrefix}.authorizationType`)}>
|
||||
<RadioGroup
|
||||
options={[
|
||||
{ value: AuthorizationType.none, label: t(`${i18nPrefix}.no-auth`) },
|
||||
{ value: AuthorizationType.apiKey, label: t(`${i18nPrefix}.api-key`) },
|
||||
]}
|
||||
value={tempPayload.type}
|
||||
onChange={handleAuthTypeChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{tempPayload.type === AuthorizationType.apiKey && (
|
||||
<>
|
||||
<Field title={t(`${i18nPrefix}.auth-type`)}>
|
||||
<RadioGroup
|
||||
options={[
|
||||
{ value: APIType.basic, label: t(`${i18nPrefix}.basic`) },
|
||||
{ value: APIType.bearer, label: t(`${i18nPrefix}.bearer`) },
|
||||
{ value: APIType.custom, label: t(`${i18nPrefix}.custom`) },
|
||||
]}
|
||||
value={tempPayload.config?.type || APIType.basic}
|
||||
onChange={handleAuthAPITypeChange}
|
||||
/>
|
||||
</Field>
|
||||
{tempPayload.config?.type === APIType.custom && (
|
||||
<Field title={t(`${i18nPrefix}.header`)} isRequired>
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
value={tempPayload.config?.header || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('header')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field title={t(`${i18nPrefix}.api-key-title`)} isRequired>
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
value={tempPayload.config?.api_key || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('api_key')}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-6 flex justify-end space-x-2'>
|
||||
<Button onClick={onHide} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.cancel')}</Button>
|
||||
<Button type='primary' onClick={handleConfirm} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(Authorization)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
type Option = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
title: string
|
||||
onClick: () => void
|
||||
isSelected: boolean
|
||||
}
|
||||
const Item: FC<ItemProps> = ({
|
||||
title,
|
||||
onClick,
|
||||
isSelected,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isSelected ? 'border-[2px] border-primary-400 bg-white shadow-xs' : 'border border-gray-100 bg-gray-25',
|
||||
'w-0 grow flex items-center justify-center h-8 cursor-pointer rounded-lg text-[13px] font-normal text-gray-900')
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
options: Option[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const RadioGroup: FC<Props> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((value: string) => {
|
||||
return () => onChange(value)
|
||||
}, [onChange])
|
||||
return (
|
||||
<div className='flex space-x-2'>
|
||||
{options.map(option => (
|
||||
<Item
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
onClick={handleChange(option.value)}
|
||||
isSelected={option.value === value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RadioGroup)
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import type { Body } from '../../types'
|
||||
import { BodyType } from '../../types'
|
||||
import useKeyValueList from '../../hooks/use-key-value-list'
|
||||
import KeyValue from '../key-value'
|
||||
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
|
||||
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
payload: Body
|
||||
onChange: (payload: Body) => void
|
||||
}
|
||||
|
||||
const allTypes = [
|
||||
BodyType.none,
|
||||
BodyType.formData,
|
||||
BodyType.xWwwFormUrlencoded,
|
||||
BodyType.rawText,
|
||||
BodyType.json,
|
||||
]
|
||||
const bodyTextMap = {
|
||||
[BodyType.none]: 'none',
|
||||
[BodyType.formData]: 'form-data',
|
||||
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
|
||||
[BodyType.rawText]: 'raw text',
|
||||
[BodyType.json]: 'JSON',
|
||||
}
|
||||
|
||||
const EditBody: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { type } = payload
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newType = e.target.value as BodyType
|
||||
onChange({
|
||||
type: newType,
|
||||
data: '',
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setBody([])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onChange])
|
||||
|
||||
const {
|
||||
list: body,
|
||||
setList: setBody,
|
||||
addItem: addBody,
|
||||
} = useKeyValueList(payload.data, (value) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
})
|
||||
onChange(newBody)
|
||||
}, type === BodyType.json)
|
||||
|
||||
const isCurrentKeyValue = type === BodyType.formData || type === BodyType.xWwwFormUrlencoded
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentKeyValue)
|
||||
return
|
||||
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = body.map((item) => {
|
||||
if (!item.key && !item.value)
|
||||
return ''
|
||||
return `${item.key}:${item.value}`
|
||||
}).join('\n')
|
||||
})
|
||||
onChange(newBody)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCurrentKeyValue])
|
||||
|
||||
const handleBodyValueChange = useCallback((value: string) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
})
|
||||
onChange(newBody)
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* body type */}
|
||||
<div className='flex flex-wrap'>
|
||||
{allTypes.map(t => (
|
||||
<label key={t} htmlFor={`body-type-${t}`} className='mr-4 flex items-center h-7 space-x-2'>
|
||||
<input
|
||||
type="radio"
|
||||
id={`body-type-${t}`}
|
||||
value={t}
|
||||
checked={type === t}
|
||||
onChange={handleTypeChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='leading-[18px] text-[13px] font-normal text-gray-700'>{bodyTextMap[t]}</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{/* body value */}
|
||||
<div className={cn(type !== BodyType.none && 'mt-1')}>
|
||||
{type === BodyType.none && null}
|
||||
{(type === BodyType.formData || type === BodyType.xWwwFormUrlencoded) && (
|
||||
<KeyValue
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
list={body}
|
||||
onChange={setBody}
|
||||
onAdd={addBody}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.rawText && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-raw'}
|
||||
title={<div className='uppercase'>Raw text</div>}
|
||||
onChange={handleBodyValueChange}
|
||||
value={payload.data}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.json && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-json'}
|
||||
title='JSON'
|
||||
value={payload.data}
|
||||
onChange={handleBodyValueChange}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditBody)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TextEditor from '@/app/components/workflow/nodes/_base/components/editor/text-editor'
|
||||
import { LayoutGrid02 } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onSwitchToKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
const BulkEdit: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSwitchToKeyValueEdit,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempValue, setTempValue] = React.useState(value)
|
||||
|
||||
const handleChange = useCallback((value: string) => {
|
||||
setTempValue(value)
|
||||
}, [])
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
onChange(tempValue)
|
||||
}, [tempValue, onChange])
|
||||
|
||||
const handleSwitchToKeyValueEdit = useCallback(() => {
|
||||
onChange(tempValue)
|
||||
onSwitchToKeyValueEdit()
|
||||
}, [tempValue, onChange, onSwitchToKeyValueEdit])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextEditor
|
||||
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
|
||||
value={tempValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
headerRight={
|
||||
<div className='flex items-center h-[18px]'>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer'
|
||||
onClick={handleSwitchToKeyValueEdit}
|
||||
>
|
||||
<LayoutGrid02 className='w-3 h-3 text-gray-500' />
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.keyValueEdit`)}</div>
|
||||
</div>
|
||||
<div className='ml-3 mr-1.5 w-px h-3 bg-gray-200'></div>
|
||||
</div>
|
||||
}
|
||||
minHeight={150}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(BulkEdit)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { KeyValue } from '../../types'
|
||||
import KeyValueEdit from './key-value-edit'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
// toggleKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
// toggleKeyValueEdit,
|
||||
}) => {
|
||||
// const handleBulkValueChange = useCallback((value: string) => {
|
||||
// const newList = value.split('\n').map((item) => {
|
||||
// const [key, value] = item.split(':')
|
||||
// return {
|
||||
// key: key ? key.trim() : '',
|
||||
// value: value ? value.trim() : '',
|
||||
// }
|
||||
// })
|
||||
// onChange(newList)
|
||||
// }, [onChange])
|
||||
|
||||
// const bulkList = (() => {
|
||||
// const res = list.map((item) => {
|
||||
// if (!item.key && !item.value)
|
||||
// return ''
|
||||
// if (!item.value)
|
||||
// return item.key
|
||||
// return `${item.key}:${item.value}`
|
||||
// }).join('\n')
|
||||
// return res
|
||||
// })()
|
||||
return <KeyValueEdit
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
list={list}
|
||||
onChange={onChange}
|
||||
onAdd={onAdd}
|
||||
// onSwitchToBulkEdit={toggleKeyValueEdit}
|
||||
/>
|
||||
// : <BulkEdit
|
||||
// value={bulkList}
|
||||
// onChange={handleBulkValueChange}
|
||||
// onSwitchToKeyValueEdit={toggleKeyValueEdit}
|
||||
// />
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { KeyValue } from '../../../types'
|
||||
import KeyValueItem from './item'
|
||||
// import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
// onSwitchToBulkEdit: () => void
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
// onSwitchToBulkEdit,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((index: number) => {
|
||||
return (newItem: KeyValue) => {
|
||||
const newList = produce(list, (draft: any) => {
|
||||
draft[index] = newItem
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft: any) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
return (
|
||||
<div className='border border-gray-200 rounded-lg overflow-hidden'>
|
||||
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
|
||||
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
|
||||
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
|
||||
<div>{t(`${i18nPrefix}.value`)}</div>
|
||||
{/* {!readonly && (
|
||||
<TooltipPlus
|
||||
popupContent={t(`${i18nPrefix}.bulkEdit`)}
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800'
|
||||
onClick={onSwitchToBulkEdit}
|
||||
>
|
||||
<EditList className='w-3 h-3' />
|
||||
</div>
|
||||
</TooltipPlus>)} */}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
list.map((item, index) => (
|
||||
<KeyValueItem
|
||||
key={item.id}
|
||||
instanceId={item.id!}
|
||||
nodeId={nodeId}
|
||||
payload={item}
|
||||
onChange={handleChange(index)}
|
||||
onRemove={handleRemove(index)}
|
||||
isLastItem={index === list.length - 1}
|
||||
onAdd={onAdd}
|
||||
readonly={readonly}
|
||||
canRemove={list.length > 1}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAvailableVarList from '../../../../_base/hooks/use-available-var-list'
|
||||
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
type Props = {
|
||||
className?: string
|
||||
instanceId?: string
|
||||
nodeId: string
|
||||
value: string
|
||||
onChange: (newValue: string) => void
|
||||
hasRemove: boolean
|
||||
onRemove?: () => void
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const InputItem: FC<Props> = ({
|
||||
className,
|
||||
instanceId,
|
||||
nodeId,
|
||||
value,
|
||||
onChange,
|
||||
hasRemove,
|
||||
onRemove,
|
||||
placeholder,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const hasValue = !!value
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const handleRemove = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onRemove?.()
|
||||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'hover:bg-gray-50 hover:cursor-text', 'relative flex h-full items-center')}>
|
||||
{(!readOnly)
|
||||
? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'bg-gray-100' : 'bg-width', 'w-0 grow px-3 py-1')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)
|
||||
: <div
|
||||
className="pl-0.5 w-full h-[18px] leading-[18px]"
|
||||
>
|
||||
{!hasValue && <div className='text-gray-300 text-xs font-normal'>{placeholder}</div>}
|
||||
{hasValue && (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>}
|
||||
{hasRemove && !isFocus && (
|
||||
<RemoveButton
|
||||
className='group-hover:block hidden absolute right-1 top-0.5'
|
||||
onClick={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputItem)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import type { KeyValue } from '../../../types'
|
||||
import InputItem from './input-item'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
instanceId: string
|
||||
className?: string
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
canRemove: boolean
|
||||
payload: KeyValue
|
||||
onChange: (newPayload: KeyValue) => void
|
||||
onRemove: () => void
|
||||
isLastItem: boolean
|
||||
onAdd: () => void
|
||||
}
|
||||
|
||||
const KeyValueItem: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
nodeId,
|
||||
readonly,
|
||||
canRemove,
|
||||
payload,
|
||||
onChange,
|
||||
onRemove,
|
||||
isLastItem,
|
||||
onAdd,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: string) => {
|
||||
const newPayload = produce(payload, (draft: any) => {
|
||||
draft[key] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
if (key === 'value' && isLastItem)
|
||||
onAdd()
|
||||
}
|
||||
}, [onChange, onAdd, isLastItem, payload])
|
||||
|
||||
return (
|
||||
// group class name is for hover row show remove button
|
||||
<div className={cn(className, 'group flex items-start h-min-7 border-t border-gray-200')}>
|
||||
<div className='w-1/2 h-full border-r border-gray-200'>
|
||||
<InputItem
|
||||
instanceId={`http-key-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.key}
|
||||
onChange={handleChange('key')}
|
||||
hasRemove={false}
|
||||
placeholder={t(`${i18nPrefix}.key`)!}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-1/2 h-full'>
|
||||
<InputItem
|
||||
instanceId={`http-value-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.value}
|
||||
onChange={handleChange('value')}
|
||||
hasRemove={!readonly && canRemove}
|
||||
onRemove={onRemove}
|
||||
placeholder={t(`${i18nPrefix}.value`)!}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueItem)
|
||||
48
web/app/components/workflow/nodes/http/default.ts
Normal file
48
web/app/components/workflow/nodes/http/default.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { AuthorizationType, BodyType, type HttpNodeType, Method } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
method: Method.get,
|
||||
url: '',
|
||||
authorization: {
|
||||
type: AuthorizationType.none,
|
||||
config: null,
|
||||
},
|
||||
headers: '',
|
||||
params: '',
|
||||
body: {
|
||||
type: BodyType.none,
|
||||
data: '',
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: HttpNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
if (!errorMessages && !payload.url)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.api') })
|
||||
|
||||
if (!errorMessages && !payload.url.startsWith('http://') && !payload.url.startsWith('https://'))
|
||||
errorMessages = t('workflow.nodes.http.notStartWithHttp')
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { uniqueId } from 'lodash'
|
||||
import type { KeyValue } from '../types'
|
||||
|
||||
const UNIQUE_ID_PREFIX = 'key-value-'
|
||||
const strToKeyValueList = (value: string) => {
|
||||
return value.split('\n').map((item) => {
|
||||
const [key, value] = item.split(':')
|
||||
return {
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
key: key.trim(),
|
||||
value: value?.trim(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const useKeyValueList = (value: string, onChange: (value: string) => void, noFilter?: boolean) => {
|
||||
const [list, doSetList] = useState<KeyValue[]>(value ? strToKeyValueList(value) : [])
|
||||
const setList = (l: KeyValue[]) => {
|
||||
doSetList(l.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
id: item.id || uniqueId(UNIQUE_ID_PREFIX),
|
||||
}
|
||||
}))
|
||||
}
|
||||
useEffect(() => {
|
||||
if (noFilter)
|
||||
return
|
||||
const newValue = list.filter(item => item.key && item.value).map(item => `${item.key}:${item.value}`).join('\n')
|
||||
if (newValue !== value)
|
||||
onChange(newValue)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list, noFilter])
|
||||
const addItem = useCallback(() => {
|
||||
setList([...list, {
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
key: '',
|
||||
value: '',
|
||||
}])
|
||||
}, [list])
|
||||
|
||||
const [isKeyValueEdit, {
|
||||
toggle: toggleIsKeyValueEdit,
|
||||
}] = useBoolean(true)
|
||||
|
||||
return {
|
||||
list: list.length === 0 ? [{ id: uniqueId(UNIQUE_ID_PREFIX), key: '', value: '' }] : list, // no item can not add new item
|
||||
setList,
|
||||
addItem,
|
||||
isKeyValueEdit,
|
||||
toggleIsKeyValueEdit,
|
||||
}
|
||||
}
|
||||
|
||||
export default useKeyValueList
|
||||
29
web/app/components/workflow/nodes/http/node.tsx
Normal file
29
web/app/components/workflow/nodes/http/node.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var'
|
||||
import type { HttpNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
const Node: FC<NodeProps<HttpNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { method, url } = data
|
||||
if (!url)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='flex items-start p-1 rounded-md bg-gray-100'>
|
||||
<div className='flex items-center h-4 shrink-0 px-1 rounded bg-gray-25 text-xs font-semibold text-gray-700 uppercase'>{method}</div>
|
||||
<div className='pl-1'>
|
||||
<ReadonlyInputWithSelectVar
|
||||
value={url}
|
||||
nodeId={id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
171
web/app/components/workflow/nodes/http/panel.tsx
Normal file
171
web/app/components/workflow/nodes/http/panel.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import useConfig from './use-config'
|
||||
import ApiInput from './components/api-input'
|
||||
import KeyValue from './components/key-value'
|
||||
import EditBody from './components/edit-body'
|
||||
import AuthorizationModal from './components/authorization'
|
||||
import type { HttpNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleMethodChange,
|
||||
handleUrlChange,
|
||||
headers,
|
||||
setHeaders,
|
||||
addHeader,
|
||||
params,
|
||||
setParams,
|
||||
addParam,
|
||||
setBody,
|
||||
isShowAuthorization,
|
||||
showAuthorization,
|
||||
hideAuthorization,
|
||||
setAuthorization,
|
||||
// single run
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
runResult,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.api`)}
|
||||
operations={
|
||||
<div
|
||||
onClick={showAuthorization}
|
||||
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
|
||||
>
|
||||
{!readOnly && <Settings01 className='w-3 h-3 text-gray-500' />}
|
||||
<div className='text-xs font-medium text-gray-500'>
|
||||
{t(`${i18nPrefix}.authorization.authorization`)}
|
||||
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ApiInput
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
method={inputs.method}
|
||||
onMethodChange={handleMethodChange}
|
||||
url={inputs.url}
|
||||
onUrlChange={handleUrlChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.headers`)}
|
||||
>
|
||||
<KeyValue
|
||||
nodeId={id}
|
||||
list={headers}
|
||||
onChange={setHeaders}
|
||||
onAdd={addHeader}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.params`)}
|
||||
>
|
||||
<KeyValue
|
||||
nodeId={id}
|
||||
list={params}
|
||||
onChange={setParams}
|
||||
onAdd={addParam}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.body`)}
|
||||
>
|
||||
<EditBody
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
payload={inputs.body}
|
||||
onChange={setBody}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{(isShowAuthorization && !readOnly) && (
|
||||
<AuthorizationModal
|
||||
isShow
|
||||
onHide={hideAuthorization}
|
||||
payload={inputs.authorization}
|
||||
onChange={setAuthorization}
|
||||
/>
|
||||
)}
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='body'
|
||||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.body`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='status_code'
|
||||
type='number'
|
||||
description={t(`${i18nPrefix}.outputVars.statusCode`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='headers'
|
||||
type='object'
|
||||
description={t(`${i18nPrefix}.outputVars.headers`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='files'
|
||||
type='Array[File]'
|
||||
description={t(`${i18nPrefix}.outputVars.files`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: varInputs,
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||
/>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
59
web/app/components/workflow/nodes/http/types.ts
Normal file
59
web/app/components/workflow/nodes/http/types.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export enum Method {
|
||||
get = 'get',
|
||||
post = 'post',
|
||||
head = 'head',
|
||||
patch = 'patch',
|
||||
put = 'put',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
export enum BodyType {
|
||||
none = 'none',
|
||||
formData = 'form-data',
|
||||
xWwwFormUrlencoded = 'x-www-form-urlencoded',
|
||||
rawText = 'raw-text',
|
||||
json = 'json',
|
||||
}
|
||||
|
||||
export type KeyValue = {
|
||||
id?: string
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type Body = {
|
||||
type: BodyType
|
||||
data: string
|
||||
}
|
||||
|
||||
export enum AuthorizationType {
|
||||
none = 'no-auth',
|
||||
apiKey = 'api-key',
|
||||
}
|
||||
|
||||
export enum APIType {
|
||||
basic = 'basic',
|
||||
bearer = 'bearer',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export type Authorization = {
|
||||
type: AuthorizationType
|
||||
config?: {
|
||||
type: APIType
|
||||
api_key: string
|
||||
header?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export type HttpNodeType = CommonNodeType & {
|
||||
variables: Variable[]
|
||||
method: Method
|
||||
url: string
|
||||
headers: string
|
||||
params: string
|
||||
body: Body
|
||||
authorization: Authorization
|
||||
}
|
||||
164
web/app/components/workflow/nodes/http/use-config.ts
Normal file
164
web/app/components/workflow/nodes/http/use-config.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import { VarType } from '../../types'
|
||||
import type { Var } from '../../types'
|
||||
import type { Authorization, Body, HttpNodeType, Method } from './types'
|
||||
import useKeyValueList from './hooks/use-key-value-list'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload)
|
||||
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<HttpNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const handleMethodChange = useCallback((method: Method) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.method = method
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUrlChange = useCallback((url: string) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.url = url
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleFieldChange = useCallback((field: string) => {
|
||||
return (value: string) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
(draft as any)[field] = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const {
|
||||
list: headers,
|
||||
setList: setHeaders,
|
||||
addItem: addHeader,
|
||||
isKeyValueEdit: isHeaderKeyValueEdit,
|
||||
toggleIsKeyValueEdit: toggleIsHeaderKeyValueEdit,
|
||||
} = useKeyValueList(inputs.headers, handleFieldChange('headers'))
|
||||
|
||||
const {
|
||||
list: params,
|
||||
setList: setParams,
|
||||
addItem: addParam,
|
||||
isKeyValueEdit: isParamKeyValueEdit,
|
||||
toggleIsKeyValueEdit: toggleIsParamKeyValueEdit,
|
||||
} = useKeyValueList(inputs.params, handleFieldChange('params'))
|
||||
|
||||
const setBody = useCallback((data: Body) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.body = data
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// authorization
|
||||
const [isShowAuthorization, {
|
||||
setTrue: showAuthorization,
|
||||
setFalse: hideAuthorization,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const setAuthorization = useCallback((authorization: Authorization) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.authorization = authorization
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
} = useOneStepRun<HttpNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
defaultRunInputData: {},
|
||||
})
|
||||
|
||||
const varInputs = getInputVars([
|
||||
inputs.url,
|
||||
inputs.headers,
|
||||
inputs.params,
|
||||
inputs.body.data,
|
||||
])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
filterVar,
|
||||
handleMethodChange,
|
||||
handleUrlChange,
|
||||
// headers
|
||||
headers,
|
||||
setHeaders,
|
||||
addHeader,
|
||||
isHeaderKeyValueEdit,
|
||||
toggleIsHeaderKeyValueEdit,
|
||||
// params
|
||||
params,
|
||||
setParams,
|
||||
addParam,
|
||||
isParamKeyValueEdit,
|
||||
toggleIsParamKeyValueEdit,
|
||||
// body
|
||||
setBody,
|
||||
// authorization
|
||||
isShowAuthorization,
|
||||
showAuthorization,
|
||||
hideAuthorization,
|
||||
setAuthorization,
|
||||
// single run
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
runResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
5
web/app/components/workflow/nodes/http/utils.ts
Normal file
5
web/app/components/workflow/nodes/http/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { HttpNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: HttpNodeType) => {
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import VarReferencePicker from '../../_base/components/variable/var-reference-picker'
|
||||
import { isComparisonOperatorNeedTranslate } from '../utils'
|
||||
import { VarType } from '../../../types'
|
||||
import type { Condition } from '@/app/components/workflow/nodes/if-else/types'
|
||||
import { ComparisonOperator, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { RefreshCw05 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Selector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const Line = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="2" viewBox="0 0 163 2" fill="none">
|
||||
<path d="M0 1H162.5" stroke="url(#paint0_linear_641_36452)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_641_36452" x1="162.5" y1="9.99584" x2="6.6086e-06" y2="9.94317" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#F3F4F6" />
|
||||
<stop offset="1" stopColor="#F3F4F6" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const getOperators = (type?: VarType) => {
|
||||
switch (type) {
|
||||
case VarType.string:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.number:
|
||||
return [
|
||||
ComparisonOperator.equal,
|
||||
ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan,
|
||||
ComparisonOperator.lessThan,
|
||||
ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThanOrEqual,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.arrayString:
|
||||
case VarType.arrayNumber:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.array:
|
||||
case VarType.arrayObject:
|
||||
return [
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
default:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
payload: Condition
|
||||
varType?: VarType
|
||||
onChange: (newItem: Condition) => void
|
||||
canRemove: boolean
|
||||
onRemove?: () => void
|
||||
isShowLogicalOperator?: boolean
|
||||
logicalOperator: LogicalOperator
|
||||
onLogicalOperatorToggle: () => void
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
payload,
|
||||
varType,
|
||||
onChange,
|
||||
canRemove,
|
||||
onRemove = () => { },
|
||||
isShowLogicalOperator,
|
||||
logicalOperator,
|
||||
onLogicalOperatorToggle,
|
||||
filterVar,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isValueReadOnly = payload.comparison_operator ? [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(payload.comparison_operator) : false
|
||||
|
||||
const handleVarReferenceChange = useCallback((value: ValueSelector | string) => {
|
||||
onChange({
|
||||
...payload,
|
||||
variable_selector: value as ValueSelector,
|
||||
})
|
||||
}, [onChange, payload])
|
||||
|
||||
// change to default operator if the variable type is changed
|
||||
useEffect(() => {
|
||||
if (varType && payload.comparison_operator) {
|
||||
if (!getOperators(varType).includes(payload.comparison_operator)) {
|
||||
onChange({
|
||||
...payload,
|
||||
comparison_operator: getOperators(varType)[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [varType, payload])
|
||||
|
||||
const handleValueChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...payload,
|
||||
value: e.target.value,
|
||||
})
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleComparisonOperatorChange = useCallback((v: ComparisonOperator) => {
|
||||
onChange({
|
||||
...payload,
|
||||
comparison_operator: v,
|
||||
})
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{isShowLogicalOperator && (
|
||||
<div className='flex items-center select-none'>
|
||||
<div className='flex items-center '>
|
||||
{Line}
|
||||
<div
|
||||
className='shrink-0 mx-1 flex items-center h-[22px] pl-2 pr-1.5 border border-gray-200 rounded-lg bg-white shadow-xs space-x-0.5 text-primary-600 cursor-pointer'
|
||||
onClick={onLogicalOperatorToggle}
|
||||
>
|
||||
<div className='text-xs font-semibold uppercase'>{t(`${i18nPrefix}.${logicalOperator === LogicalOperator.and ? 'and' : 'or'}`)}</div>
|
||||
<RefreshCw05 className='w-3 h-3' />
|
||||
</div>
|
||||
<div className=' rotate-180'>
|
||||
{Line}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className='flex items-center space-x-1'>
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
readonly={readonly}
|
||||
isShowNodeName
|
||||
className='w-[162px]'
|
||||
value={payload.variable_selector}
|
||||
onChange={handleVarReferenceChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
|
||||
<Selector
|
||||
popupClassName='top-[34px]'
|
||||
itemClassName='capitalize'
|
||||
trigger={
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (readonly) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (!varType) {
|
||||
e.stopPropagation()
|
||||
Toast.notify({
|
||||
message: t(`${i18nPrefix}.notSetVariable`),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}}
|
||||
className={cn(!readonly && 'cursor-pointer', 'shrink-0 w-[100px] whitespace-nowrap flex items-center h-8 justify-between px-2.5 rounded-lg bg-gray-100 capitalize')}
|
||||
>
|
||||
{
|
||||
!payload.comparison_operator
|
||||
? <div className='text-[13px] font-normal text-gray-400'>{t(`${i18nPrefix}.operator`)}</div>
|
||||
: <div className='text-[13px] font-normal text-gray-900'>{isComparisonOperatorNeedTranslate(payload.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${payload.comparison_operator}`) : payload.comparison_operator}</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
readonly={readonly}
|
||||
value={payload.comparison_operator || ''}
|
||||
options={getOperators(varType).map((o) => {
|
||||
return {
|
||||
label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o,
|
||||
value: o,
|
||||
}
|
||||
})}
|
||||
onChange={handleComparisonOperatorChange}
|
||||
/>
|
||||
|
||||
<input
|
||||
readOnly={readonly || isValueReadOnly || !varType}
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
|
||||
if (!varType) {
|
||||
Toast.notify({
|
||||
message: t(`${i18nPrefix}.notSetVariable`),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}}
|
||||
value={!isValueReadOnly ? payload.value : ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder={(!readonly && !isValueReadOnly) ? t(`${i18nPrefix}.enterValue`)! : ''}
|
||||
className='w-[80px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text'
|
||||
/>
|
||||
{!readonly && (
|
||||
<div
|
||||
className={cn(canRemove ? 'text-gray-500 bg-gray-100 hover:bg-gray-200 cursor-pointer' : 'bg-gray-25 text-gray-300', 'p-2 rounded-lg ')}
|
||||
onClick={canRemove ? onRemove : () => { }}
|
||||
>
|
||||
<Trash03 className='w-4 h-4 ' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div >
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import type { Var, VarType } from '../../../types'
|
||||
import Item from './condition-item'
|
||||
import type { Condition, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
className?: string
|
||||
readonly: boolean
|
||||
list: Condition[]
|
||||
varTypesList: (VarType | undefined)[]
|
||||
onChange: (newList: Condition[]) => void
|
||||
logicalOperator: LogicalOperator
|
||||
onLogicalOperatorToggle: () => void
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
}
|
||||
|
||||
const ConditionList: FC<Props> = ({
|
||||
className,
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
varTypesList,
|
||||
onChange,
|
||||
logicalOperator,
|
||||
onLogicalOperatorToggle,
|
||||
filterVar,
|
||||
}) => {
|
||||
const handleItemChange = useCallback((index: number) => {
|
||||
return (newItem: Condition) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index] = newItem
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleItemRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const canRemove = list.length > 1
|
||||
|
||||
if (list.length === 0)
|
||||
return null
|
||||
return (
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
<Item
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
payload={list[0]}
|
||||
varType={varTypesList[0]}
|
||||
onChange={handleItemChange(0)}
|
||||
canRemove={canRemove}
|
||||
onRemove={handleItemRemove(0)}
|
||||
logicalOperator={logicalOperator}
|
||||
onLogicalOperatorToggle={onLogicalOperatorToggle}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
{
|
||||
list.length > 1 && (
|
||||
list.slice(1).map((item, i) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
payload={item}
|
||||
varType={varTypesList[i + 1]}
|
||||
onChange={handleItemChange(i + 1)}
|
||||
canRemove={canRemove}
|
||||
onRemove={handleItemRemove(i + 1)}
|
||||
isShowLogicalOperator
|
||||
logicalOperator={logicalOperator}
|
||||
onLogicalOperatorToggle={onLogicalOperatorToggle}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
)))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConditionList)
|
||||
53
web/app/components/workflow/nodes/if-else/default.ts
Normal file
53
web/app/components/workflow/nodes/if-else/default.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { BlockEnum, type NodeDefault } from '../../types'
|
||||
import { type IfElseNodeType, LogicalOperator } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<IfElseNodeType> = {
|
||||
defaultValue: {
|
||||
_targetBranches: [
|
||||
{
|
||||
id: 'true',
|
||||
name: 'IS TRUE',
|
||||
},
|
||||
{
|
||||
id: 'false',
|
||||
name: 'IS FALSE',
|
||||
},
|
||||
],
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.VariableAssigner)
|
||||
},
|
||||
checkValid(payload: IfElseNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { conditions } = payload
|
||||
if (!conditions || conditions.length === 0)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: 'IF' })
|
||||
|
||||
conditions.forEach((condition) => {
|
||||
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
if (!errorMessages && !condition.comparison_operator)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.ifElse.operator') })
|
||||
if (!errorMessages && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
})
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
61
web/app/components/workflow/nodes/if-else/node.tsx
Normal file
61
web/app/components/workflow/nodes/if-else/node.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { NodeSourceHandle } from '../_base/components/node-handle'
|
||||
import { isComparisonOperatorNeedTranslate, isEmptyRelatedOperator } from './utils'
|
||||
import type { IfElseNodeType } from './types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
||||
const { data } = props
|
||||
const { t } = useTranslation()
|
||||
const { conditions, logical_operator } = data
|
||||
|
||||
return (
|
||||
<div className='px-3'>
|
||||
<div className='relative flex items-center h-6 px-1'>
|
||||
<div className='w-full text-xs font-semibold text-right text-gray-700'>IF</div>
|
||||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId='true'
|
||||
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-0.5'>
|
||||
{conditions.map((condition, i) => (
|
||||
<div key={condition.id} className='relative'>
|
||||
{(condition.variable_selector?.length > 0 && condition.comparison_operator && (isEmptyRelatedOperator(condition.comparison_operator!) ? true : !!condition.value))
|
||||
? (
|
||||
<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-gray-700 bg-gray-100 rounded-md'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-primary-500' />
|
||||
<span>{condition.variable_selector.slice(-1)[0]}</span>
|
||||
<span className='text-gray-500'>{isComparisonOperatorNeedTranslate(condition.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${condition.comparison_operator}`) : condition.comparison_operator}</span>
|
||||
{!isEmptyRelatedOperator(condition.comparison_operator!) && <span>{condition.value}</span>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-gray-500 bg-gray-100 rounded-md'>
|
||||
{t(`${i18nPrefix}.conditionNotSetup`)}
|
||||
</div>
|
||||
)}
|
||||
{i !== conditions.length - 1 && (
|
||||
<div className='absolute z-10 right-0 bottom-[-10px] leading-4 text-[10px] font-medium text-primary-600 uppercase'>{t(`${i18nPrefix}.${logical_operator}`)}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='relative flex items-center h-6 px-1'>
|
||||
<div className='w-full text-xs font-semibold text-right text-gray-700'>ELSE</div>
|
||||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId='false'
|
||||
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(IfElseNode)
|
||||
66
web/app/components/workflow/nodes/if-else/panel.tsx
Normal file
66
web/app/components/workflow/nodes/if-else/panel.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Split from '../_base/components/split'
|
||||
import AddButton from '../_base/components/add-button'
|
||||
import useConfig from './use-config'
|
||||
import ConditionList from './components/condition-list'
|
||||
import type { IfElseNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleConditionsChange,
|
||||
handleAddCondition,
|
||||
handleLogicalOperatorToggle,
|
||||
varTypesList,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.if`)}
|
||||
>
|
||||
<>
|
||||
<ConditionList
|
||||
className='mt-2'
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
list={inputs.conditions}
|
||||
onChange={handleConditionsChange}
|
||||
logicalOperator={inputs.logical_operator}
|
||||
onLogicalOperatorToggle={handleLogicalOperatorToggle}
|
||||
varTypesList={varTypesList}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<AddButton
|
||||
className='mt-3'
|
||||
text={t(`${i18nPrefix}.addCondition`)}
|
||||
onClick={handleAddCondition}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.else`)}
|
||||
>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-400'>{t(`${i18nPrefix}.elseDescription`)}</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
37
web/app/components/workflow/nodes/if-else/types.ts
Normal file
37
web/app/components/workflow/nodes/if-else/types.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export enum LogicalOperator {
|
||||
and = 'and',
|
||||
or = 'or',
|
||||
}
|
||||
|
||||
export enum ComparisonOperator {
|
||||
contains = 'contains',
|
||||
notContains = 'not contains',
|
||||
startWith = 'start with',
|
||||
endWith = 'end with',
|
||||
is = 'is',
|
||||
isNot = 'is not',
|
||||
empty = 'empty',
|
||||
notEmpty = 'not empty',
|
||||
equal = '=',
|
||||
notEqual = '≠',
|
||||
largerThan = '>',
|
||||
lessThan = '<',
|
||||
largerThanOrEqual = '≥',
|
||||
lessThanOrEqual = '≤',
|
||||
isNull = 'is null',
|
||||
isNotNull = 'is not null',
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
id: string
|
||||
variable_selector: ValueSelector
|
||||
comparison_operator?: ComparisonOperator
|
||||
value: string
|
||||
}
|
||||
|
||||
export type IfElseNodeType = CommonNodeType & {
|
||||
logical_operator: LogicalOperator
|
||||
conditions: Condition[]
|
||||
}
|
||||
68
web/app/components/workflow/nodes/if-else/use-config.ts
Normal file
68
web/app/components/workflow/nodes/if-else/use-config.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import { getVarType } from '../_base/components/variable/utils'
|
||||
import { LogicalOperator } from './types'
|
||||
import type { Condition, IfElseNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const isChatMode = useIsChatMode()
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload)
|
||||
|
||||
const handleConditionsChange = useCallback((newConditions: Condition[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.conditions = newConditions
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddCondition = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.conditions.push({
|
||||
id: `${Date.now()}`,
|
||||
variable_selector: [],
|
||||
comparison_operator: undefined,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleLogicalOperatorToggle = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type !== VarType.arrayFile
|
||||
}, [])
|
||||
|
||||
const varTypesList = (inputs.conditions || []).map((condition) => {
|
||||
return getVarType(condition.variable_selector, availableNodes, isChatMode)
|
||||
})
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleConditionsChange,
|
||||
handleAddCondition,
|
||||
handleLogicalOperatorToggle,
|
||||
varTypesList,
|
||||
filterVar,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
17
web/app/components/workflow/nodes/if-else/utils.ts
Normal file
17
web/app/components/workflow/nodes/if-else/utils.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { ComparisonOperator } from './types'
|
||||
|
||||
export const isEmptyRelatedOperator = (operator: ComparisonOperator) => {
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(operator)
|
||||
}
|
||||
|
||||
const notTranslateKey = [
|
||||
ComparisonOperator.equal, ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan, ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThan, ComparisonOperator.lessThanOrEqual,
|
||||
]
|
||||
|
||||
export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator) => {
|
||||
if (!operator)
|
||||
return false
|
||||
return !notTranslateKey.includes(operator)
|
||||
}
|
||||
36
web/app/components/workflow/nodes/index.tsx
Normal file
36
web/app/components/workflow/nodes/index.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { memo } from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { Node } from '../types'
|
||||
import {
|
||||
NodeComponentMap,
|
||||
PanelComponentMap,
|
||||
} from './constants'
|
||||
import BaseNode from './_base/node'
|
||||
import BasePanel from './_base/panel'
|
||||
|
||||
const CustomNode = memo((props: NodeProps) => {
|
||||
const nodeData = props.data
|
||||
const NodeComponent = NodeComponentMap[nodeData.type]
|
||||
|
||||
return (
|
||||
<BaseNode { ...props }>
|
||||
<NodeComponent />
|
||||
</BaseNode>
|
||||
)
|
||||
})
|
||||
CustomNode.displayName = 'CustomNode'
|
||||
|
||||
export const Panel = memo((props: Node) => {
|
||||
const nodeData = props.data
|
||||
const PanelComponent = PanelComponentMap[nodeData.type]
|
||||
|
||||
return (
|
||||
<BasePanel key={props.id} {...props}>
|
||||
<PanelComponent />
|
||||
</BasePanel>
|
||||
)
|
||||
})
|
||||
|
||||
Panel.displayName = 'Panel'
|
||||
|
||||
export default memo(CustomNode)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import SelectDataset from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
selectedIds: string[]
|
||||
onChange: (dataSets: DataSet[]) => void
|
||||
}
|
||||
|
||||
const AddDataset: FC<Props> = ({
|
||||
selectedIds,
|
||||
onChange,
|
||||
}) => {
|
||||
const [isShowModal, {
|
||||
setTrue: showModal,
|
||||
setFalse: hideModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleSelect = useCallback((datasets: DataSet[]) => {
|
||||
onChange(datasets)
|
||||
hideModal()
|
||||
}, [onChange, hideModal])
|
||||
return (
|
||||
<div>
|
||||
<AddButton onClick={showModal} />
|
||||
{isShowModal && (
|
||||
<SelectDataset
|
||||
isShow={isShowModal}
|
||||
onClose={hideModal}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddDataset)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { Settings01, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import FileIcon from '@/app/components/base/file-icon'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import SettingsModal from '@/app/components/app/configuration/dataset-config/settings-modal'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type Props = {
|
||||
payload: DataSet
|
||||
onRemove: () => void
|
||||
onChange: (dataSet: DataSet) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const DatasetItem: FC<Props> = ({
|
||||
payload,
|
||||
onRemove,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [isShowSettingsModal, {
|
||||
setTrue: showSettingsModal,
|
||||
setFalse: hideSettingsModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleSave = useCallback((newDataset: DataSet) => {
|
||||
onChange(newDataset)
|
||||
hideSettingsModal()
|
||||
}, [hideSettingsModal, onChange])
|
||||
|
||||
return (
|
||||
<div className='flex items-center h-10 justify-between rounded-xl px-2 bg-white border border-gray-200 cursor-pointer group/dataset-item'>
|
||||
<div className='w-0 grow flex items-center space-x-1.5'>
|
||||
{
|
||||
payload.data_source_type === DataSourceType.NOTION
|
||||
? (
|
||||
<div className='shrink-0 flex items-center justify-center w-6 h-6 rounded-md border-[0.5px] border-[#EAECF5]'>
|
||||
<FileIcon type='notion' className='w-4 h-4' />
|
||||
</div>
|
||||
)
|
||||
: <div className='shrink-0 flex items-center justify-center w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]'>
|
||||
<Folder className='w-4 h-4 text-[#444CE7]' />
|
||||
</div>
|
||||
}
|
||||
<div className='w-0 grow text-[13px] font-normal text-gray-800 truncate'>{payload.name}</div>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='hidden group-hover/dataset-item:flex shrink-0 ml-2 items-center space-x-1'>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={showSettingsModal}
|
||||
>
|
||||
<Settings01 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShowSettingsModal && (
|
||||
<Drawer isOpen={isShowSettingsModal} onClose={hideSettingsModal} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<SettingsModal
|
||||
currentDataset={payload}
|
||||
onCancel={hideSettingsModal}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetItem)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Item from './dataset-item'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
type Props = {
|
||||
list: DataSet[]
|
||||
onChange: (list: DataSet[]) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const DatasetList: FC<Props> = ({
|
||||
list,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleChange = useCallback((index: number) => {
|
||||
return (value: DataSet) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index] = value
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
{list.length
|
||||
? list.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
payload={item}
|
||||
onRemove={handleRemove(index)}
|
||||
onChange={handleChange(index)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: (
|
||||
<div className='p-3 text-xs text-center text-gray-500 rounded-lg cursor-default select-none bg-gray-50'>
|
||||
{t('appDebug.datasetConfig.knowledgeTip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetList)
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { MultipleRetrievalConfig, SingleRetrievalConfig } from '../types'
|
||||
import type { ModelConfig } from '../../../types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import ConfigRetrievalContent from '@/app/components/app/configuration/dataset-config/params-config/config-content'
|
||||
import { RETRIEVE_TYPE } from '@/types/app'
|
||||
import { DATASET_DEFAULT } from '@/config'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
import type {
|
||||
DatasetConfigs,
|
||||
} from '@/models/debug'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type Props = {
|
||||
payload: {
|
||||
retrieval_mode: RETRIEVE_TYPE
|
||||
multiple_retrieval_config?: MultipleRetrievalConfig
|
||||
single_retrieval_config?: SingleRetrievalConfig
|
||||
}
|
||||
onRetrievalModeChange: (mode: RETRIEVE_TYPE) => void
|
||||
onMultipleRetrievalConfigChange: (config: MultipleRetrievalConfig) => void
|
||||
singleRetrievalModelConfig?: ModelConfig
|
||||
onSingleRetrievalModelChange?: (config: ModelConfig) => void
|
||||
onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const RetrievalConfig: FC<Props> = ({
|
||||
payload,
|
||||
onRetrievalModeChange,
|
||||
onMultipleRetrievalConfigChange,
|
||||
singleRetrievalModelConfig,
|
||||
onSingleRetrievalModelChange,
|
||||
onSingleRetrievalModelParamsChange,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const {
|
||||
defaultModel: rerankDefaultModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
|
||||
|
||||
const { multiple_retrieval_config } = payload
|
||||
const handleChange = useCallback((configs: DatasetConfigs, isRetrievalModeChange?: boolean) => {
|
||||
if (isRetrievalModeChange) {
|
||||
onRetrievalModeChange(configs.retrieval_model)
|
||||
return
|
||||
}
|
||||
onMultipleRetrievalConfigChange({
|
||||
top_k: configs.top_k,
|
||||
score_threshold: configs.score_threshold_enabled ? (configs.score_threshold || DATASET_DEFAULT.score_threshold) : null,
|
||||
reranking_model: payload.retrieval_mode === RETRIEVE_TYPE.oneWay
|
||||
? undefined
|
||||
: (!configs.reranking_model?.reranking_provider_name
|
||||
? {
|
||||
provider: rerankDefaultModel?.provider?.provider || '',
|
||||
model: rerankDefaultModel?.model || '',
|
||||
}
|
||||
: {
|
||||
provider: configs.reranking_model?.reranking_provider_name,
|
||||
model: configs.reranking_model?.reranking_model_name,
|
||||
}),
|
||||
})
|
||||
}, [onMultipleRetrievalConfigChange, payload.retrieval_mode, rerankDefaultModel?.provider?.provider, rerankDefaultModel?.model, onRetrievalModeChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
// mainAxis: 12,
|
||||
crossAxis: -2,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
>
|
||||
<div className={cn(!readonly && 'cursor-pointer', open && 'bg-gray-100', 'flex items-center h-6 px-2 rounded-md hover:bg-gray-100 group select-none')}>
|
||||
<div className={cn(open ? 'text-gray-700' : 'text-gray-500', 'leading-[18px] text-xs font-medium group-hover:bg-gray-100')}>{payload.retrieval_mode === RETRIEVE_TYPE.oneWay ? t('appDebug.datasetConfig.retrieveOneWay.title') : t('appDebug.datasetConfig.retrieveMultiWay.title')}</div>
|
||||
{!readonly && <ChevronDown className='w-3 h-3 ml-1' />}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 1001 }}>
|
||||
<div className='w-[404px] pt-3 pb-4 px-4 shadow-xl rounded-2xl border border-gray-200 bg-white'>
|
||||
<ConfigRetrievalContent
|
||||
datasetConfigs={
|
||||
{
|
||||
retrieval_model: payload.retrieval_mode,
|
||||
reranking_model: !multiple_retrieval_config?.reranking_model?.provider
|
||||
? {
|
||||
reranking_provider_name: rerankDefaultModel?.provider?.provider || '',
|
||||
reranking_model_name: rerankDefaultModel?.model || '',
|
||||
}
|
||||
: {
|
||||
reranking_provider_name: multiple_retrieval_config?.reranking_model?.provider || '',
|
||||
reranking_model_name: multiple_retrieval_config?.reranking_model?.model || '',
|
||||
},
|
||||
top_k: multiple_retrieval_config?.top_k || DATASET_DEFAULT.top_k,
|
||||
score_threshold_enabled: !(multiple_retrieval_config?.score_threshold === undefined || multiple_retrieval_config?.score_threshold === null),
|
||||
score_threshold: multiple_retrieval_config?.score_threshold,
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
onChange={handleChange}
|
||||
isInWorkflow
|
||||
singleRetrievalModelConfig={singleRetrievalModelConfig}
|
||||
onSingleRetrievalModelChange={onSingleRetrievalModelChange}
|
||||
onSingleRetrievalModelParamsChange={onSingleRetrievalModelParamsChange}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(RetrievalConfig)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { KnowledgeRetrievalNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
import { RETRIEVE_TYPE } from '@/types/app'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<KnowledgeRetrievalNodeType> = {
|
||||
defaultValue: {
|
||||
query_variable_selector: [],
|
||||
dataset_ids: [],
|
||||
retrieval_mode: RETRIEVE_TYPE.oneWay,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: KnowledgeRetrievalNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && (!payload.query_variable_selector || payload.query_variable_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.knowledgeRetrieval.queryVariable`) })
|
||||
|
||||
if (!errorMessages && (!payload.dataset_ids || payload.dataset_ids.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.knowledgeRetrieval.knowledge`) })
|
||||
|
||||
if (!errorMessages && payload.retrieval_mode === RETRIEVE_TYPE.multiWay && !payload.multiple_retrieval_config?.reranking_model?.provider)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.errorMsg.fields.rerankModel`) })
|
||||
|
||||
if (!errorMessages && payload.retrieval_mode === RETRIEVE_TYPE.oneWay && !payload.single_retrieval_config?.model?.provider)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t('common.modelProvider.systemReasoningModel.key') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { type FC, useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import type { KnowledgeRetrievalNodeType } from './types'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
const Node: FC<NodeProps<KnowledgeRetrievalNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (data.dataset_ids?.length > 0) {
|
||||
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: data.dataset_ids } })
|
||||
setSelectedDatasets(dataSetsWithDetail)
|
||||
}
|
||||
else {
|
||||
setSelectedDatasets([])
|
||||
}
|
||||
})()
|
||||
}, [data.dataset_ids])
|
||||
|
||||
if (!selectedDatasets.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='space-y-0.5'>
|
||||
{selectedDatasets.map(({ id, name }) => (
|
||||
<div key={id} className='flex items-center h-[26px] bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700'>
|
||||
<div className='mr-1 shrink-0 p-1 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]'>
|
||||
<Folder className='w-3 h-3 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
165
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
Normal file
165
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import useConfig from './use-config'
|
||||
import RetrievalConfig from './components/retrieval-config'
|
||||
import AddKnowledge from './components/add-dataset'
|
||||
import DatasetList from './components/dataset-list'
|
||||
import type { KnowledgeRetrievalNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.knowledgeRetrieval'
|
||||
|
||||
const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleQueryVarChange,
|
||||
filterVar,
|
||||
handleModelChanged,
|
||||
handleCompletionParamsChange,
|
||||
handleRetrievalModeChange,
|
||||
handleMultipleRetrievalConfigChange,
|
||||
selectedDatasets,
|
||||
handleOnDatasetsChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
query,
|
||||
setQuery,
|
||||
runResult,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
{/* {JSON.stringify(inputs, null, 2)} */}
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.queryVariable`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
value={inputs.query_variable_selector}
|
||||
onChange={handleQueryVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.knowledge`)}
|
||||
operations={
|
||||
<div className='flex items-center space-x-1'>
|
||||
<RetrievalConfig
|
||||
payload={{
|
||||
retrieval_mode: inputs.retrieval_mode,
|
||||
multiple_retrieval_config: inputs.multiple_retrieval_config,
|
||||
single_retrieval_config: inputs.single_retrieval_config,
|
||||
}}
|
||||
onRetrievalModeChange={handleRetrievalModeChange}
|
||||
onMultipleRetrievalConfigChange={handleMultipleRetrievalConfigChange}
|
||||
singleRetrievalModelConfig={inputs.single_retrieval_config?.model}
|
||||
onSingleRetrievalModelChange={handleModelChanged as any}
|
||||
onSingleRetrievalModelParamsChange={handleCompletionParamsChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
{!readOnly && (<div className='w-px h-3 bg-gray-200'></div>)}
|
||||
{!readOnly && (
|
||||
<AddKnowledge
|
||||
selectedIds={inputs.dataset_ids}
|
||||
onChange={handleOnDatasetsChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DatasetList
|
||||
list={selectedDatasets}
|
||||
onChange={handleOnDatasetsChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='result'
|
||||
type='Array[Object]'
|
||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||
subItems={[
|
||||
{
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: t(`${i18nPrefix}.outputVars.content`),
|
||||
},
|
||||
// url, title, link like bing search reference result: link, link page title, link page icon
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
description: t(`${i18nPrefix}.outputVars.title`),
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
description: t(`${i18nPrefix}.outputVars.url`),
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
type: 'string',
|
||||
description: t(`${i18nPrefix}.outputVars.icon`),
|
||||
},
|
||||
{
|
||||
name: 'metadata',
|
||||
type: 'object',
|
||||
description: t(`${i18nPrefix}.outputVars.metadata`),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
</>
|
||||
</OutputVars>
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: [{
|
||||
label: t(`${i18nPrefix}.queryVariable`)!,
|
||||
variable: 'query',
|
||||
type: InputVarType.paragraph,
|
||||
required: true,
|
||||
}],
|
||||
values: { query },
|
||||
onChange: keyValue => setQuery((keyValue as any).query),
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { CommonNodeType, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { RETRIEVE_TYPE } from '@/types/app'
|
||||
|
||||
export type MultipleRetrievalConfig = {
|
||||
top_k: number
|
||||
score_threshold: number | null | undefined
|
||||
reranking_model?: {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SingleRetrievalConfig = {
|
||||
model: ModelConfig
|
||||
}
|
||||
|
||||
export type KnowledgeRetrievalNodeType = CommonNodeType & {
|
||||
query_variable_selector: ValueSelector
|
||||
dataset_ids: string[]
|
||||
retrieval_mode: RETRIEVE_TYPE
|
||||
multiple_retrieval_config?: MultipleRetrievalConfig
|
||||
single_retrieval_config?: SingleRetrievalConfig
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import {
|
||||
useIsChatMode, useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import type { KnowledgeRetrievalNodeType, MultipleRetrievalConfig } from './types'
|
||||
import { RETRIEVE_TYPE } from '@/types/app'
|
||||
import { DATASET_DEFAULT } from '@/config'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const startNode = getBeforeNodesInSameBranch(id).find(node => node.data.type === BlockEnum.Start)
|
||||
const startNodeId = startNode?.id
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<KnowledgeRetrievalNodeType>(id, payload)
|
||||
|
||||
const setInputs = useCallback((s: KnowledgeRetrievalNodeType) => {
|
||||
const newInputs = produce(s, (draft) => {
|
||||
if (s.retrieval_mode === RETRIEVE_TYPE.multiWay)
|
||||
delete draft.single_retrieval_config
|
||||
else
|
||||
delete draft.multiple_retrieval_config
|
||||
})
|
||||
// not work in pass to draft...
|
||||
doSetInputs(newInputs)
|
||||
}, [doSetInputs])
|
||||
|
||||
const inputRef = useRef(inputs)
|
||||
useEffect(() => {
|
||||
inputRef.current = inputs
|
||||
}, [inputs])
|
||||
|
||||
const handleQueryVarChange = useCallback((newVar: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.query_variable_selector = newVar as ValueSelector
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
const {
|
||||
defaultModel: rerankDefaultModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
|
||||
|
||||
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
if (!draft.single_retrieval_config) {
|
||||
draft.single_retrieval_config = {
|
||||
model: {
|
||||
provider: '',
|
||||
name: '',
|
||||
mode: '',
|
||||
completion_params: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
const draftModel = draft.single_retrieval_config?.model
|
||||
draftModel.provider = model.provider
|
||||
draftModel.name = model.modelId
|
||||
draftModel.mode = model.mode!
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
|
||||
// inputRef.current.single_retrieval_config?.model is old when change the provider...
|
||||
if (isEqual(newParams, inputRef.current.single_retrieval_config?.model.completion_params))
|
||||
return
|
||||
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
if (!draft.single_retrieval_config) {
|
||||
draft.single_retrieval_config = {
|
||||
model: {
|
||||
provider: '',
|
||||
name: '',
|
||||
mode: '',
|
||||
completion_params: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
draft.single_retrieval_config.model.completion_params = newParams
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
// set defaults models
|
||||
useEffect(() => {
|
||||
const inputs = inputRef.current
|
||||
if (inputs.retrieval_mode === RETRIEVE_TYPE.multiWay && inputs.multiple_retrieval_config?.reranking_model?.provider)
|
||||
return
|
||||
|
||||
if (inputs.retrieval_mode === RETRIEVE_TYPE.oneWay && inputs.single_retrieval_config?.model?.provider)
|
||||
return
|
||||
|
||||
const newInput = produce(inputs, (draft) => {
|
||||
if (currentProvider?.provider && currentModel?.model) {
|
||||
const hasSetModel = draft.single_retrieval_config?.model?.provider
|
||||
if (!hasSetModel) {
|
||||
draft.single_retrieval_config = {
|
||||
model: {
|
||||
provider: currentProvider?.provider,
|
||||
name: currentModel?.model,
|
||||
mode: currentModel?.model_properties?.mode as string,
|
||||
completion_params: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const multipleRetrievalConfig = draft.multiple_retrieval_config
|
||||
draft.multiple_retrieval_config = {
|
||||
top_k: multipleRetrievalConfig?.top_k || DATASET_DEFAULT.top_k,
|
||||
score_threshold: multipleRetrievalConfig?.score_threshold,
|
||||
reranking_model: payload.retrieval_mode === RETRIEVE_TYPE.oneWay
|
||||
? undefined
|
||||
: (!multipleRetrievalConfig?.reranking_model?.provider
|
||||
? {
|
||||
provider: rerankDefaultModel?.provider?.provider || '',
|
||||
model: rerankDefaultModel?.model || '',
|
||||
}
|
||||
: multipleRetrievalConfig?.reranking_model),
|
||||
}
|
||||
})
|
||||
setInputs(newInput)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentProvider?.provider, currentModel, rerankDefaultModel])
|
||||
|
||||
const handleRetrievalModeChange = useCallback((newMode: RETRIEVE_TYPE) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.retrieval_mode = newMode
|
||||
if (newMode === RETRIEVE_TYPE.multiWay) {
|
||||
draft.multiple_retrieval_config = {
|
||||
top_k: draft.multiple_retrieval_config?.top_k || DATASET_DEFAULT.top_k,
|
||||
score_threshold: draft.multiple_retrieval_config?.score_threshold,
|
||||
reranking_model: !draft.multiple_retrieval_config?.reranking_model?.provider
|
||||
? {
|
||||
provider: rerankDefaultModel?.provider?.provider || '',
|
||||
model: rerankDefaultModel?.model || '',
|
||||
}
|
||||
: draft.multiple_retrieval_config?.reranking_model,
|
||||
}
|
||||
}
|
||||
else {
|
||||
const hasSetModel = draft.single_retrieval_config?.model?.provider
|
||||
if (!hasSetModel) {
|
||||
draft.single_retrieval_config = {
|
||||
model: {
|
||||
provider: currentProvider?.provider || '',
|
||||
name: currentModel?.model || '',
|
||||
mode: currentModel?.model_properties?.mode as string,
|
||||
completion_params: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [currentModel?.model, currentModel?.model_properties?.mode, currentProvider?.provider, inputs, rerankDefaultModel?.model, rerankDefaultModel?.provider?.provider, setInputs])
|
||||
|
||||
const handleMultipleRetrievalConfigChange = useCallback((newConfig: MultipleRetrievalConfig) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.multiple_retrieval_config = newConfig
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// datasets
|
||||
const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const inputs = inputRef.current
|
||||
const datasetIds = inputs.dataset_ids
|
||||
if (datasetIds?.length > 0) {
|
||||
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } })
|
||||
setSelectedDatasets(dataSetsWithDetail)
|
||||
}
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.dataset_ids = datasetIds
|
||||
})
|
||||
setInputs(newInputs)
|
||||
})()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let query_variable_selector: ValueSelector = inputs.query_variable_selector
|
||||
if (isChatMode && inputs.query_variable_selector.length === 0 && startNodeId)
|
||||
query_variable_selector = [startNodeId, 'sys.query']
|
||||
|
||||
setInputs({
|
||||
...inputs,
|
||||
query_variable_selector,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleOnDatasetsChange = useCallback((newDatasets: DataSet[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.dataset_ids = newDatasets.map(d => d.id)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
setSelectedDatasets(newDatasets)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.string
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
} = useOneStepRun<KnowledgeRetrievalNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
defaultRunInputData: {
|
||||
query: '',
|
||||
},
|
||||
})
|
||||
|
||||
const query = runInputData.query
|
||||
const setQuery = useCallback((newQuery: string) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
query: newQuery,
|
||||
})
|
||||
}, [runInputData, setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleQueryVarChange,
|
||||
filterVar,
|
||||
handleRetrievalModeChange,
|
||||
handleMultipleRetrievalConfigChange,
|
||||
handleModelChanged,
|
||||
handleCompletionParamsChange,
|
||||
selectedDatasets,
|
||||
handleOnDatasetsChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
query,
|
||||
setQuery,
|
||||
runResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue