Compare commits

...

67 Commits

Author SHA1 Message Date
Joel
4386c9f4f1 main 2025-08-25 10:35:52 +08:00
Joel
62955f4d11 chore: hide json entrance 2025-08-25 10:26:11 +08:00
Joel
e38ba8403d feat: workflow support input json 2025-08-22 14:54:34 +08:00
Joel
a1f278bdc8 fix: one step run form 2025-08-22 14:43:23 +08:00
Joel
65c0f8d657 feat: webapp support json field 2025-08-22 14:36:12 +08:00
Joel
c3e4d5a059 feat: support choose and save deep obj 2025-08-21 15:31:34 +08:00
Joel
30775109fd feat: can edit and choose obj 2025-08-21 14:47:29 +08:00
Joel
76659843d7 fix: item value type 2025-08-20 16:32:29 +08:00
Joel
42258bf451 chore: node tracing node value 2025-08-20 16:24:53 +08:00
Joel
8462c79179 fix: can not use converstation vars in var aggregator node 2025-08-20 14:59:58 +08:00
Joel
f5f11f5a9e feat: basic input field edit 2025-08-20 14:23:33 +08:00
Joel
96e8321462 chore: add var can not has exist key 2025-08-20 14:06:46 +08:00
Joel
af6e5e8663 chore: edit filed exists 2025-08-20 14:00:38 +08:00
Joel
d4f976270d fix: chore: if boolean array bug 2025-08-19 16:16:10 +08:00
Joel
dc3cbd9a74 chore: handle duliplicate keys 2025-08-19 15:57:18 +08:00
Joel
056564c100 merge main 2025-08-19 15:48:01 +08:00
Joel
d5b99610fc chore: iteratino input support bool list 2025-08-07 10:18:45 +08:00
Joel
04d2b0775d fix: chatbot bool 2025-08-04 11:14:33 +08:00
Joel
6614e97b53 fix: basic chatbot bool 2025-08-04 10:59:20 +08:00
Joel
ca37e304cc fix: basic bool error 2025-08-04 10:53:35 +08:00
Joel
437729db97 feat: support var arrs 2025-08-01 18:36:18 +08:00
Joel
fbd0effa04 chore: handle bool value in vars 2025-08-01 18:26:58 +08:00
Joel
14c62850e0 fix: list filter operation filter boolean problem 2025-08-01 18:06:27 +08:00
Joel
c4824a1d4c fix: assigner set false can not public 2025-08-01 17:55:20 +08:00
Joel
d642e9a98e chore: fix can not publish 2025-08-01 17:39:25 +08:00
Joel
72c6195c10 fix: list operation boolean default 2025-08-01 17:32:09 +08:00
Joel
2bd096b454 fix: con vars set value 2025-08-01 17:24:53 +08:00
Joel
6287e3cd9e fix: boolean can picker var 2025-07-31 10:28:17 +08:00
Joel
94c33c6eed chore: boolean string to boolean 2025-07-30 17:56:59 +08:00
Joel
6d03a15e0f chore: start form boolean to checkbox 2025-07-30 10:53:12 +08:00
Joel
bd04ddd544 merge main 2025-07-30 10:26:31 +08:00
Joel
cb51360aa3 chore: remove arr bool 2025-07-29 11:34:16 +08:00
Joel
269d34304e fix : loop var default value 2025-07-28 16:17:52 +08:00
Joel
590c14c977 chore: conveersion pass boolean string boolean 2025-07-28 16:02:50 +08:00
Joel
5fbeb275b2 fix: loop not choose bool var value 2025-07-28 15:20:18 +08:00
Joel
ff9d051635 chore: fix if not value set value 2025-07-28 14:49:44 +08:00
Joel
1e18c828d9 chore: remove transform boolean to string in editor 2025-07-28 14:09:34 +08:00
Joel
5b895be412 feat: template support bools input 2025-07-28 13:46:55 +08:00
Joel
ea2a1b203c fix: list operator bool not show the bool input 2025-07-28 11:20:22 +08:00
Joel
7bfdfe73c2 chore: list filter bool value 2025-07-28 11:09:37 +08:00
Joel
171c5055f8 fix: loop arry bool inner item type value 2025-07-25 14:52:13 +08:00
Joel
e3e4369358 fix: webapp get boolean value 2025-07-25 14:05:14 +08:00
Joel
efa28453be fix: if value show 2025-07-24 17:54:22 +08:00
Joel
37de3b1b68 fix: single run bool type 2025-07-24 17:39:10 +08:00
Joel
75d4e519fc chore: loop false can run 2025-07-24 16:06:13 +08:00
Joel
cf63926e16 chore: assigner node 2025-07-24 15:59:44 +08:00
Joel
14eb5b7930 feat: list operator support bool 2025-07-24 15:15:35 +08:00
Joel
544ebde054 chore: if bool value problem 2025-07-24 15:05:09 +08:00
Joel
6012ad57ac feat: code support boolean 2025-07-24 14:35:41 +08:00
Joel
70f6d8b42a main 2025-07-24 14:10:29 +08:00
Joel
1d738f3fa6 feat: chat support bool 2025-07-09 16:48:17 +08:00
Joel
912d68a148 chore: competion webapp 2025-07-09 14:56:11 +08:00
Joel
8c8c250570 feat: completion support boolean 2025-07-09 14:28:36 +08:00
Joel
b5782fff8f feat: basic chat app support bool 2025-07-09 14:06:25 +08:00
Joel
f2dfb5363f feat: one step run support boolean 2025-07-09 11:23:21 +08:00
Joel
fc0dea5647 feat: workflow debug add bool 2025-07-09 10:53:52 +08:00
Joel
fcf0ae4c85 feat: extract parameter support bool 2025-07-08 16:41:58 +08:00
Joel
9ac629bcf5 feat: if node support bool 2025-07-08 16:30:54 +08:00
Joel
79bdb22f79 feat: loop termin support bool 2025-07-08 16:06:56 +08:00
Joel
b72d977871 feat: loop var support bool type 2025-07-08 15:23:24 +08:00
Joel
657d3d1dae fix: boolean problem 2025-07-08 14:34:18 +08:00
Joel
3ba23238e5 chore: boolarray in varibale 2025-07-04 17:47:44 +08:00
Joel
e1a7b59160 chore: missing files 2025-07-04 16:48:56 +08:00
Joel
18941beb34 feat: converstation support bool 2025-07-04 16:48:33 +08:00
Joel
6ff18d13e6 feat: struct output boolean 2025-07-04 16:12:47 +08:00
Joel
1094b3da23 feat: add var type 2025-07-04 16:02:28 +08:00
Joel
77aa35ff15 feat: add boolean type 2025-07-04 15:27:52 +08:00
84 changed files with 1073 additions and 243 deletions

View File

@@ -0,0 +1,24 @@
export const jsonObjectWrap = {
type: 'object',
properties: {},
required: [],
additionalProperties: true,
}
export const jsonConfigPlaceHolder = JSON.stringify(
{
foo: {
type: 'string',
},
bar: {
type: 'object',
properties: {
sub: {
type: 'number',
},
},
required: [],
additionalProperties: true,
},
}, null, 2,
)

View File

@@ -2,21 +2,28 @@
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
title: string
isOptional?: boolean
children: React.JSX.Element
}
const Field: FC<Props> = ({
className,
title,
isOptional,
children,
}) => {
const { t } = useTranslation()
return (
<div className={cn(className)}>
<div className='system-sm-semibold leading-8 text-text-secondary'>{title}</div>
<div className='system-sm-semibold leading-8 text-text-secondary'>
{title}
{isOptional && <span className='system-xs-regular ml-1 text-text-tertiary'>({t('appDebug.variableConfig.optional')})</span>}
</div>
<div>{children}</div>
</div>
)

View File

@@ -1,13 +1,12 @@
'use client'
import type { ChangeEvent, FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import ModalFoot from '../modal-foot'
import ConfigSelect from '../config-select'
import ConfigString from '../config-string'
import SelectTypeItem from '../select-type-item'
import Field from './field'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
@@ -20,7 +19,13 @@ import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/
import Checkbox from '@/app/components/base/checkbox'
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import type { Item as SelectItem } from './type-select'
import TypeSelector from './type-select'
import { SimpleSelect } from '@/app/components/base/select'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
import { useStore as useAppStore } from '@/app/components/app/store'
import Textarea from '@/app/components/base/textarea'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { TransferMethod } from '@/types/app'
@@ -51,6 +56,20 @@ const ConfigModal: FC<IConfigModalProps> = ({
const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any)
const { type, label, variable, options, max_length } = tempPayload
const modalRef = useRef<HTMLDivElement>(null)
const appDetail = useAppStore(state => state.appDetail)
const isBasicApp = appDetail?.mode !== 'advanced-chat' && appDetail?.mode !== 'workflow'
const isSupportJSON = false
const jsonSchemaStr = useMemo(() => {
const isJsonObject = type === InputVarType.jsonObject
if (!isJsonObject || !tempPayload.json_schema)
return ''
try {
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
}
catch (_e) {
return ''
}
}, [tempPayload.json_schema])
useEffect(() => {
// To fix the first input element auto focus, then directly close modal will raise error
if (isShow)
@@ -82,25 +101,74 @@ const ConfigModal: FC<IConfigModalProps> = ({
}
}, [])
const handleTypeChange = useCallback((type: InputVarType) => {
return () => {
const newPayload = produce(tempPayload, (draft) => {
draft.type = type
// Clear default value when switching types
draft.default = undefined
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
const handleJSONSchemaChange = useCallback((value: string) => {
try {
const v = JSON.parse(value)
const res = {
...jsonObjectWrap,
properties: v,
}
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
}
catch (_e) {
return null
}
}, [handlePayloadChange])
const selectOptions: SelectItem[] = [
{
name: t('appDebug.variableConfig.text-input'),
value: InputVarType.textInput,
},
{
name: t('appDebug.variableConfig.paragraph'),
value: InputVarType.paragraph,
},
{
name: t('appDebug.variableConfig.select'),
value: InputVarType.select,
},
{
name: t('appDebug.variableConfig.number'),
value: InputVarType.number,
},
{
name: t('appDebug.variableConfig.checkbox'),
value: InputVarType.checkbox,
},
...(supportFile ? [
{
name: t('appDebug.variableConfig.single-file'),
value: InputVarType.singleFile,
},
{
name: t('appDebug.variableConfig.multi-files'),
value: InputVarType.multiFiles,
},
] : []),
...((!isBasicApp && isSupportJSON) ? [{
name: t('appDebug.variableConfig.json'),
value: InputVarType.jsonObject,
}] : []),
]
const handleTypeChange = useCallback((item: SelectItem) => {
const type = item.value as InputVarType
const newPayload = produce(tempPayload, (draft) => {
draft.type = type
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
}, [tempPayload])
const handleVarKeyBlur = useCallback((e: any) => {
@@ -142,15 +210,6 @@ const ConfigModal: FC<IConfigModalProps> = ({
if (!isVariableNameValid)
return
// TODO: check if key already exists. should the consider the edit case
// if (varKeys.map(key => key?.trim()).includes(tempPayload.variable.trim())) {
// Toast.notify({
// type: 'error',
// message: t('appDebug.varKeyError.keyAlreadyExists', { key: tempPayload.variable }),
// })
// return
// }
if (!tempPayload.label) {
Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.labelNameRequired') })
return
@@ -204,18 +263,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
>
<div className='mb-8' ref={modalRef} tabIndex={-1}>
<div className='space-y-2'>
<Field title={t('appDebug.variableConfig.fieldType')}>
<div className='grid grid-cols-3 gap-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={handleTypeChange(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={handleTypeChange(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={handleTypeChange(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={handleTypeChange(InputVarType.number)} />
{supportFile && <>
<SelectTypeItem type={InputVarType.singleFile} selected={type === InputVarType.singleFile} onClick={handleTypeChange(InputVarType.singleFile)} />
<SelectTypeItem type={InputVarType.multiFiles} selected={type === InputVarType.multiFiles} onClick={handleTypeChange(InputVarType.multiFiles)} />
</>}
</div>
<TypeSelector value={type} items={selectOptions} onSelect={handleTypeChange} />
</Field>
<Field title={t('appDebug.variableConfig.varName')}>
@@ -330,6 +379,21 @@ const ConfigModal: FC<IConfigModalProps> = ({
</>
)}
{type === InputVarType.jsonObject && (
<Field title={t('appDebug.variableConfig.jsonSchema')} isOptional>
<CodeEditor
language={CodeLanguage.json}
value={jsonSchemaStr}
onChange={handleJSONSchemaChange}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{jsonConfigPlaceHolder}</div>
}
/>
</Field>
)}
<div className='!mt-5 flex h-6 items-center space-x-2'>
<Checkbox checked={tempPayload.required} disabled={tempPayload.hide} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.required')}</span>

View File

@@ -0,0 +1,97 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import type { InputVarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
export type Item = {
value: InputVarType
name: string
}
type Props = {
value: string | number
onSelect: (value: Item) => void
items: Item[]
popupClassName?: string
popupInnerClassName?: string
readonly?: boolean
hideChecked?: boolean
}
const TypeSelector: FC<Props> = ({
value,
onSelect,
items,
popupInnerClassName,
readonly,
}) => {
const [open, setOpen] = useState(false)
const selectedItem = value ? items.find(item => item.value === value) : undefined
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)} className='w-full'>
<div
className={classNames(`group flex h-9 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2 text-sm hover:bg-state-base-hover-alt ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}`)}
title={selectedItem?.name}
>
<div className='flex items-center'>
<InputVarTypeIcon type={selectedItem?.value as InputVarType} className='size-4 shrink-0 text-text-secondary' />
<span
className={`
ml-1.5 ${!selectedItem?.name && 'text-components-input-text-placeholder'}
`}
>
{selectedItem?.name}
</span>
</div>
<div className='flex items-center space-x-1'>
<Badge uppercase={false}>{inputVarTypeToVarType(selectedItem?.value as InputVarType)}</Badge>
<ChevronDownIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[61]'>
<div
className={classNames('w-[432px] rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)}
>
{items.map((item: Item) => (
<div
key={item.value}
className={'flex h-9 cursor-pointer items-center justify-between rounded-lg px-2 text-text-secondary hover:bg-state-base-hover'}
title={item.name}
onClick={() => {
onSelect(item)
setOpen(false)
}}
>
<div className='flex items-center space-x-2'>
<InputVarTypeIcon type={item.value} className='size-4 shrink-0 text-text-secondary' />
<span title={item.name}>{item.name}</span>
</div>
<Badge uppercase={false}>{inputVarTypeToVarType(item.value)}</Badge>
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default TypeSelector

View File

@@ -12,7 +12,7 @@ import SelectVarType from './select-var-type'
import Tooltip from '@/app/components/base/tooltip'
import type { PromptVariable } from '@/models/debug'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { getNewVar } from '@/utils/var'
import { getNewVar, hasDuplicateStr } from '@/utils/var'
import Toast from '@/app/components/base/toast'
import Confirm from '@/app/components/base/confirm'
import ConfigContext from '@/context/debug-configuration'
@@ -80,7 +80,28 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
delete draft[currIndex].options
})
const newList = newPromptVariables
let errorMsgKey = ''
let typeName = ''
if (hasDuplicateStr(newList.map(item => item.key))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.varName'
}
else if (hasDuplicateStr(newList.map(item => item.name as string))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.labelName'
}
if (errorMsgKey) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { key: t(typeName) }),
})
return false
}
onPromptVariablesChange?.(newPromptVariables)
return true
}
const { setShowExternalDataToolModal } = useModalContext()
@@ -190,7 +211,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => {
// setCurrKey(key)
setCurrIndex(index)
if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number') {
if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number' && type !== 'checkbox') {
handleOpenExternalDataToolModal({ key, type, index, name, config, icon, icon_background }, promptVariables)
return
}
@@ -245,7 +266,8 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
isShow={isShowEditModal}
onClose={hideEditModal}
onConfirm={(item) => {
updatePromptVariableItem(item)
const isValid = updatePromptVariableItem(item)
if (!isValid) return
hideEditModal()
}}
varKeys={promptVariables.map(v => v.key)}

View File

@@ -65,6 +65,7 @@ const SelectVarType: FC<Props> = ({
<SelectItem type={InputVarType.paragraph} value='paragraph' text={t('appDebug.variableConfig.paragraph')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConfig.select')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConfig.number')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.checkbox} value='checkbox' text={t('appDebug.variableConfig.checkbox')} onClick={handleChange}></SelectItem>
</div>
<div className='h-px border-t border-components-panel-border'></div>
<div className='p-1'>

View File

@@ -120,6 +120,8 @@ const SettingBuiltInTool: FC<Props> = ({
return t('tools.setBuiltInTools.number')
if (type === 'text-input')
return t('tools.setBuiltInTools.string')
if (type === 'checkbox')
return 'boolean'
if (type === 'file')
return t('tools.setBuiltInTools.file')
return type

View File

@@ -8,6 +8,7 @@ import Textarea from '@/app/components/base/textarea'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import type { Inputs } from '@/models/debug'
import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
type Props = {
inputs: Inputs
@@ -31,7 +32,7 @@ const ChatUserInput = ({
return obj
})()
const handleInputValueChange = (key: string, value: string) => {
const handleInputValueChange = (key: string, value: string | boolean) => {
if (!(key in promptVariableObj))
return
@@ -55,10 +56,12 @@ const ChatUserInput = ({
className='mb-4 last-of-type:mb-0'
>
<div>
{type !== 'checkbox' && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
)}
<div className='grow'>
{type === 'string' && (
<Input
@@ -96,6 +99,14 @@ const ChatUserInput = ({
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'checkbox' && (
<BoolInput
name={name || key}
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
/>
)}
</div>
</div>
</div>

View File

@@ -34,7 +34,7 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows
import TooltipPlus from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { formatBooleanInputs, promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config'
import type { Inputs } from '@/models/debug'
@@ -259,7 +259,7 @@ const Debug: FC<IDebug> = ({
}
const data: Record<string, any> = {
inputs,
inputs: formatBooleanInputs(modelConfig.configs.prompt_variables, inputs),
model_config: postModelConfig,
}

View File

@@ -60,7 +60,6 @@ import {
useModelListAndDefaultModelAndCurrentProviderAndModel,
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import type { Collection } from '@/app/components/tools/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
@@ -82,6 +81,7 @@ import { supportFunctionCall } from '@/utils/tool-call'
import { MittProvider } from '@/context/mitt-context'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import Toast from '@/app/components/base/toast'
import { fetchCollectionList } from '@/service/tools'
import { useAppContext } from '@/context/app-context'
type PublishConfig = {

View File

@@ -22,6 +22,7 @@ import type { VisionFile, VisionSettings } from '@/types/app'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
export type IPromptValuePanelProps = {
appType: AppType
@@ -66,7 +67,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
else { return !modelConfig.configs.prompt_template }
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
const handleInputValueChange = (key: string, value: string) => {
const handleInputValueChange = (key: string, value: string | boolean) => {
if (!(key in promptVariableObj))
return
@@ -109,10 +110,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
className='mb-4 last-of-type:mb-0'
>
<div>
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
{type !== 'checkbox' && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
)}
<div className='grow'>
{type === 'string' && (
<Input
@@ -151,6 +154,14 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'checkbox' && (
<BoolInput
name={name || key}
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
/>
)}
</div>
</div>
</div>

View File

@@ -23,6 +23,7 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested
import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
import { formatBooleanInputs } from '@/utils/model-config'
import Avatar from '../../avatar'
const ChatWrapper = () => {
@@ -89,7 +90,7 @@ const ChatWrapper = () => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
@@ -131,7 +132,7 @@ const ChatWrapper = () => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
inputs: formatBooleanInputs(inputsForms, currentConversationId ? currentConversationInputs : newConversationInputs),
conversation_id: currentConversationId,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}

View File

@@ -222,6 +222,14 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
type: 'number',
}
}
if(item.checkbox) {
return {
...item.checkbox,
default: false,
type: 'checkbox',
}
}
if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return {
@@ -245,6 +253,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
}
if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}
let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length)
@@ -340,7 +355,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)

View File

@@ -6,6 +6,9 @@ import Textarea from '@/app/components/base/textarea'
import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
type Props = {
showTip?: boolean
@@ -42,12 +45,14 @@ const InputsFormContent = ({ showTip }: Props) => {
<div className='space-y-4'>
{visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'>
<div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
{form.type !== InputVarType.checkbox && (
<div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
)}
{form.type === InputVarType.textInput && (
<Input
value={inputsFormValue?.[form.variable] || ''}
@@ -70,6 +75,14 @@ const InputsFormContent = ({ showTip }: Props) => {
placeholder={form.label}
/>
)}
{form.type === InputVarType.checkbox && (
<BoolInput
name={form.label}
value={!!inputsFormValue?.[form.variable]}
required={form.required}
onChange={value => handleFormChange(form.variable, value)}
/>
)}
{form.type === InputVarType.select && (
<PortalSelect
popupClassName='w-[200px]'
@@ -105,6 +118,18 @@ const InputsFormContent = ({ showTip }: Props) => {
}}
/>
)}
{form.type === InputVarType.jsonObject && (
<CodeEditor
language={CodeLanguage.json}
value={inputsFormValue?.[form.variable] || ''}
onChange={v => handleFormChange(form.variable, v)}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{form.json_schema}</div>
}
/>
)}
</div>
))}
{showTip && (

View File

@@ -12,7 +12,7 @@ export const useCheckInputsForms = () => {
const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForm.filter(({ required }) => required)
const requiredVars = inputsForm.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked
if (requiredVars?.length) {
requiredVars.forEach(({ variable, label, type }) => {

View File

@@ -31,6 +31,12 @@ export const getProcessedInputs = (inputs: Record<string, any>, inputsForm: Inpu
inputsForm.forEach((item) => {
const inputValue = inputs[item.variable]
// set boolean type default value
if(item.type === InputVarType.checkbox) {
processedInputs[item.variable] = !!inputValue
return
}
if (!inputValue)
return

View File

@@ -90,7 +90,7 @@ const ChatWrapper = () => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)

View File

@@ -195,6 +195,13 @@ export const useEmbeddedChatbot = () => {
type: 'number',
}
}
if (item.checkbox) {
return {
...item.checkbox,
default: false,
type: 'checkbox',
}
}
if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return {
@@ -218,6 +225,13 @@ export const useEmbeddedChatbot = () => {
}
}
if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}
let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length)
@@ -312,7 +326,7 @@ export const useEmbeddedChatbot = () => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)

View File

@@ -6,6 +6,9 @@ import Textarea from '@/app/components/base/textarea'
import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
type Props = {
showTip?: boolean
@@ -42,12 +45,14 @@ const InputsFormContent = ({ showTip }: Props) => {
<div className='space-y-4'>
{visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'>
{form.type !== InputVarType.checkbox && (
<div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
)}
{form.type === InputVarType.textInput && (
<Input
value={inputsFormValue?.[form.variable] || ''}
@@ -70,6 +75,14 @@ const InputsFormContent = ({ showTip }: Props) => {
placeholder={form.label}
/>
)}
{form.type === InputVarType.checkbox && (
<BoolInput
name={form.label}
value={inputsFormValue?.[form.variable]}
required={form.required}
onChange={value => handleFormChange(form.variable, value)}
/>
)}
{form.type === InputVarType.select && (
<PortalSelect
popupClassName='w-[200px]'
@@ -105,6 +118,18 @@ const InputsFormContent = ({ showTip }: Props) => {
}}
/>
)}
{form.type === InputVarType.jsonObject && (
<CodeEditor
language={CodeLanguage.json}
value={inputsFormValue?.[form.variable] || ''}
onChange={v => handleFormChange(form.variable, v)}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{form.json_schema}</div>
}
/>
)}
</div>
))}
{showTip && (

View File

@@ -24,7 +24,7 @@ export enum FormTypeEnum {
secretInput = 'secret-input',
select = 'select',
radio = 'radio',
boolean = 'boolean',
checkbox = 'checkbox',
files = 'files',
file = 'file',
modelSelector = 'model-selector',

View File

@@ -77,6 +77,13 @@ const AppInputsPanel = ({
required: false,
}
}
if(item.checkbox) {
return {
...item.checkbox,
type: 'checkbox',
required: false,
}
}
if (item.select) {
return {
...item.select,
@@ -103,6 +110,13 @@ const AppInputsPanel = ({
}
}
if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}
return {
...item['text-input'],
type: 'text-input',

View File

@@ -63,6 +63,8 @@ const StrategyDetail: FC<Props> = ({
return t('tools.setBuiltInTools.number')
if (type === 'text-input')
return t('tools.setBuiltInTools.string')
if (type === 'checkbox')
return 'boolean'
if (type === 'file')
return t('tools.setBuiltInTools.file')
if (type === 'array[tools]')

View File

@@ -21,6 +21,7 @@ import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
import {
getFilesInLogs,
} from '@/app/components/base/file-uploader/utils'
import { formatBooleanInputs } from '@/utils/model-config'
export type IResultProps = {
isWorkflow: boolean
@@ -124,7 +125,9 @@ const Result: FC<IResultProps> = ({
}
let hasEmptyInput = ''
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
if(type === 'boolean')
return false // boolean input is not required
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
}) || [] // compatible with old version
@@ -158,7 +161,7 @@ const Result: FC<IResultProps> = ({
return
const data: Record<string, any> = {
inputs,
inputs: formatBooleanInputs(promptConfig?.prompt_variables, inputs),
}
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
data.files = completionFiles.map((item) => {

View File

@@ -18,6 +18,9 @@ import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uplo
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
export type IRunOnceProps = {
siteInfo: SiteInfo
@@ -93,7 +96,9 @@ const RunOnce: FC<IRunOnceProps> = ({
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
: promptConfig.prompt_variables.map(item => (
<div className='mt-4 w-full' key={item.key}>
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
{item.type !== 'boolean' && (
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
)}
<div className='mt-1'>
{item.type === 'select' && (
<Select
@@ -118,7 +123,7 @@ const RunOnce: FC<IRunOnceProps> = ({
className='h-[104px] sm:text-xs'
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'number' && (
@@ -129,6 +134,14 @@ const RunOnce: FC<IRunOnceProps> = ({
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'boolean' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key]}
required={item.required}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'file' && (
<FileUploaderInAttachmentWrapper
value={inputs[item.key] ? [inputs[item.key]] : []}
@@ -149,6 +162,18 @@ const RunOnce: FC<IRunOnceProps> = ({
}}
/>
)}
{item.type === 'json_object' && (
<CodeEditor
language={CodeLanguage.json}
value={inputs[item.key]}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{item.json_schema}</div>
}
/>
)}
</div>
</div>
))}

View File

@@ -8,6 +8,8 @@ export const toType = (type: string) => {
return 'text-input'
case 'number':
return 'number-input'
case 'boolean':
return 'checkbox'
default:
return type
}

View File

@@ -0,0 +1,38 @@
'use client'
import Checkbox from '@/app/components/base/checkbox'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
name: string
value: boolean
required?: boolean
onChange: (value: boolean) => void
}
const BoolInput: FC<Props> = ({
value,
onChange,
name,
required,
}) => {
const { t } = useTranslation()
const handleChange = useCallback(() => {
onChange(!value)
}, [value, onChange])
return (
<div className='flex h-6 items-center gap-2'>
<Checkbox
className='!h-4 !w-4'
checked={!!value}
onCheck={handleChange}
/>
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
{name}
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
</div>
)
}
export default React.memo(BoolInput)

View File

@@ -25,6 +25,7 @@ import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import cn from '@/utils/classnames'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import BoolInput from './bool-input'
type Props = {
payload: InputVar
@@ -92,6 +93,7 @@ const FormItem: FC<Props> = ({
return ''
})()
const isBooleanType = type === InputVarType.checkbox
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
const isContext = type === InputVarType.contexts
const isIterator = type === InputVarType.iterator
@@ -113,7 +115,7 @@ const FormItem: FC<Props> = ({
return (
<div className={cn(className)}>
{!isArrayLikeType && (
{!isArrayLikeType && !isBooleanType && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
{!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
@@ -166,6 +168,15 @@ const FormItem: FC<Props> = ({
)
}
{isBooleanType && (
<BoolInput
name={payload.label as string}
value={!!value}
required={payload.required}
onChange={onChange}
/>
)}
{
type === InputVarType.json && (
<CodeEditor
@@ -176,6 +187,18 @@ const FormItem: FC<Props> = ({
/>
)
}
{ type === InputVarType.jsonObject && (
<CodeEditor
value={value}
language={CodeLanguage.json}
onChange={onChange}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{payload.json_schema}</div>
}
/>
)}
{(type === InputVarType.singleFile) && (
<FileUploaderInAttachmentWrapper
value={singleFileValue}

View File

@@ -32,6 +32,8 @@ export type BeforeRunFormProps = {
} & Partial<SpecialResultPanelProps>
function formatValue(value: string | any, type: InputVarType) {
if(type === InputVarType.checkbox)
return !!value
if(value === undefined || value === null)
return value
if (type === InputVarType.number)
@@ -87,7 +89,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
form.inputs.forEach((input) => {
const value = form.values[input.variable] as any
if (!errMsg && input.required && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
if (!errMsg && input.required && (input.type !== InputVarType.checkbox) && !(input.variable in existVarValuesInForm) && (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 && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) {

View File

@@ -64,7 +64,7 @@ const FormInputItem: FC<Props> = ({
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
const showTypeSwitch = isNumber || isObject || isArray
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable

View File

@@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiAlignLeft, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import { RiAlignLeft, RiBracesLine, RiCheckboxLine, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import { InputVarType } from '../../../types'
type Props = {
@@ -15,6 +15,8 @@ const getIcon = (type: InputVarType) => {
[InputVarType.paragraph]: RiAlignLeft,
[InputVarType.select]: RiCheckboxMultipleLine,
[InputVarType.number]: RiHashtag,
[InputVarType.checkbox]: RiCheckboxLine,
[InputVarType.jsonObject]: RiBracesLine,
[InputVarType.singleFile]: RiFileList2Line,
[InputVarType.multiFiles]: RiFileCopy2Line,
} as any)[type] || RiTextSnippet

View File

@@ -57,11 +57,13 @@ export const hasValidChildren = (children: any): boolean => {
)
}
const inputVarTypeToVarType = (type: InputVarType): VarType => {
export const inputVarTypeToVarType = (type: InputVarType): VarType => {
return ({
[InputVarType.number]: VarType.number,
[InputVarType.checkbox]: VarType.boolean,
[InputVarType.singleFile]: VarType.file,
[InputVarType.multiFiles]: VarType.arrayFile,
[InputVarType.jsonObject]: VarType.object,
} as any)[type] || VarType.string
}
@@ -228,14 +230,27 @@ const formatItem = (
variables,
} = data as StartNodeType
res.vars = variables.map((v) => {
return {
const type = inputVarTypeToVarType(v.type)
const varRes: Var = {
variable: v.variable,
type: inputVarTypeToVarType(v.type),
type,
isParagraph: v.type === InputVarType.paragraph,
isSelect: v.type === InputVarType.select,
options: v.options,
required: v.required,
}
try {
if(type === VarType.object && v.json_schema) {
varRes.children = {
schema: JSON.parse(v.json_schema),
}
}
}
catch (error) {
console.error('Error formatting variable:', error)
}
return varRes
})
if (isChatMode) {
res.vars.push({
@@ -690,6 +705,8 @@ const getIterationItemType = ({
return VarType.string
case VarType.arrayNumber:
return VarType.number
case VarType.arrayBoolean:
return VarType.boolean
case VarType.arrayObject:
return VarType.object
case VarType.array:
@@ -743,6 +760,8 @@ const getLoopItemType = ({
return VarType.number
case VarType.arrayObject:
return VarType.object
case VarType.arrayBoolean:
return VarType.boolean
case VarType.array:
return VarType.any
case VarType.arrayFile:

View File

@@ -18,7 +18,7 @@ type Props = {
onChange: (value: string) => void
}
const TYPES = [VarType.string, VarType.number, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.object]
const TYPES = [VarType.string, VarType.number, VarType.boolean, VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayObject, VarType.object]
const VarReferencePicker: FC<Props> = ({
readonly,
className,

View File

@@ -477,7 +477,7 @@ const BasePanel: FC<BasePanelProps> = ({
isRunAfterSingleRun={isRunAfterSingleRun}
updateNodeRunningStatus={updateNodeRunningStatus}
onSingleRunClicked={handleSingleRun}
nodeInfo={nodeInfo}
nodeInfo={nodeInfo!}
singleRunResult={runResult!}
isPaused={isPaused}
{...passedLogParams}

View File

@@ -67,7 +67,6 @@ const LastRun: FC<Props> = ({
updateNodeRunningStatus(hidePageOneStepFinishedStatus)
resetHidePageStatus()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus])
useEffect(() => {
@@ -77,7 +76,6 @@ const LastRun: FC<Props> = ({
useEffect(() => {
resetHidePageStatus()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeId])
const handlePageVisibilityChange = useCallback(() => {
@@ -117,7 +115,7 @@ const LastRun: FC<Props> = ({
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens}
created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by}
nodeInfo={nodeInfo}
nodeInfo={runResult as NodeTracing}
showSteps={false}
/>
</div>

View File

@@ -146,8 +146,8 @@ const useLastRun = <T>({
checkValid,
} = oneStepRunRes
const nodeInfo = runResult
const {
nodeInfo,
...singleRunParams
} = useSingleRunFormParamsHooks(blockType)({
id,
@@ -197,7 +197,6 @@ const useLastRun = <T>({
setTabType(TabType.lastRun)
setInitShowLastRunTab(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initShowLastRunTab])
const invalidLastRun = useInvalidLastRun(appId!, id)

View File

@@ -94,6 +94,8 @@ const varTypeToInputVarType = (type: VarType, {
return InputVarType.paragraph
if (type === VarType.number)
return InputVarType.number
if (type === VarType.boolean)
return InputVarType.checkbox
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
return InputVarType.json
if (type === VarType.file)
@@ -185,11 +187,11 @@ const useOneStepRun = <T>({
const isPaused = isPausedRef.current
// The backend don't support pause the single run, so the frontend handle the pause state.
if(isPaused)
if (isPaused)
return
const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded
if(!canRunLastRun) {
if (!canRunLastRun) {
doSetRunResult(data)
return
}
@@ -199,9 +201,9 @@ const useOneStepRun = <T>({
const { getNodes } = store.getState()
const nodes = getNodes()
appendNodeInspectVars(id, vars, nodes)
if(data?.status === NodeRunningStatus.Succeeded) {
if (data?.status === NodeRunningStatus.Succeeded) {
invalidLastRun()
if(isStartNode)
if (isStartNode)
invalidateSysVarValues()
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
}
@@ -218,21 +220,21 @@ const useOneStepRun = <T>({
})
}
const checkValidWrap = () => {
if(!checkValid)
if (!checkValid)
return { isValid: true, errorMessage: '' }
const res = checkValid(data, t, moreDataForCheckValid)
if(!res.isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: res.errorMessage,
})
if (!res.isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: res.errorMessage,
})
}
return res
}
@@ -251,7 +253,6 @@ const useOneStepRun = <T>({
const { isValid } = checkValidWrap()
setCanShowSingleRun(isValid)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data._isSingleRun])
useEffect(() => {
@@ -293,9 +294,9 @@ const useOneStepRun = <T>({
if (!isIteration && !isLoop) {
const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {}
if(isStartNode) {
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if(isChatMode)
if (isChatMode)
postData.conversation_id = ''
postData.inputs = inputs
@@ -317,7 +318,7 @@ const useOneStepRun = <T>({
{
onWorkflowStarted: noop,
onWorkflowFinished: (params) => {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -396,7 +397,7 @@ const useOneStepRun = <T>({
setIterationRunResult(newIterationRunResult)
},
onError: () => {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -420,7 +421,7 @@ const useOneStepRun = <T>({
{
onWorkflowStarted: noop,
onWorkflowFinished: (params) => {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -500,7 +501,7 @@ const useOneStepRun = <T>({
setLoopRunResult(newLoopRunResult)
},
onError: () => {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -522,7 +523,7 @@ const useOneStepRun = <T>({
hasError = true
invalidLastRun()
if (!isIteration && !isLoop) {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -544,11 +545,11 @@ const useOneStepRun = <T>({
})
}
}
if(isPausedRef.current)
if (isPausedRef.current)
return
if (!isIteration && !isLoop && !hasError) {
if(isPausedRef.current)
if (isPausedRef.current)
return
handleNodeDataUpdate({
id,
@@ -587,7 +588,7 @@ const useOneStepRun = <T>({
}
}
return {
label: item.label || item.variable,
label: (typeof item.label === 'object' ? item.label.variable : item.label) || item.variable,
variable: item.variable,
type: varTypeToInputVarType(originalVar.type, {
isSelect: !!originalVar.isSelect,

View File

@@ -9,13 +9,15 @@ import { AssignerNodeInputType, WriteMode } from '../../types'
import type { AssignerNodeOperation } from '../../types'
import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import type { ValueSelector, Var, VarType } from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { noop } from 'lodash-es'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
type Props = {
readonly: boolean
@@ -59,23 +61,27 @@ const VarList: FC<Props> = ({
}
}, [list, onChange])
const handleOperationChange = useCallback((index: number) => {
const handleOperationChange = useCallback((index: number, varType: VarType) => {
return (item: { value: string | number }) => {
const newList = produce(list, (draft) => {
draft[index].operation = item.value as WriteMode
draft[index].value = '' // Clear value when operation changes
if (item.value === WriteMode.set || item.value === WriteMode.increment || item.value === WriteMode.decrement
|| item.value === WriteMode.multiply || item.value === WriteMode.divide)
|| item.value === WriteMode.multiply || item.value === WriteMode.divide) {
if(varType === VarType.boolean)
draft[index].value = false
draft[index].input_type = AssignerNodeInputType.constant
else
}
else {
draft[index].input_type = AssignerNodeInputType.variable
}
})
onChange(newList)
}
}, [list, onChange])
const handleToAssignedVarChange = useCallback((index: number) => {
return (value: ValueSelector | string | number) => {
return (value: ValueSelector | string | number | boolean) => {
const newList = produce(list, (draft) => {
draft[index].value = value as ValueSelector
})
@@ -145,7 +151,7 @@ const VarList: FC<Props> = ({
value={item.operation}
placeholder='Operation'
disabled={!item.variable_selector || item.variable_selector.length === 0}
onSelect={handleOperationChange(index)}
onSelect={handleOperationChange(index, assignedVarType!)}
assignedVarType={assignedVarType}
writeModeTypes={writeModeTypes}
writeModeTypesArr={writeModeTypesArr}
@@ -188,6 +194,12 @@ const VarList: FC<Props> = ({
className='w-full'
/>
)}
{assignedVarType === 'boolean' && (
<BoolValue
value={item.value as boolean}
onChange={value => handleToAssignedVarChange(index)(value)}
/>
)}
{assignedVarType === 'object' && (
<CodeEditor
value={item.value as string}

View File

@@ -33,7 +33,7 @@ const nodeDefault: NodeDefault<AssignerNodeType> = {
if (value.operation === WriteMode.set || value.operation === WriteMode.increment
|| value.operation === WriteMode.decrement || value.operation === WriteMode.multiply
|| value.operation === WriteMode.divide) {
if (!value.value && typeof value.value !== 'number')
if (!value.value && value.value !== false && typeof value.value !== 'number')
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') })
}
else if (!value.value?.length) {

View File

@@ -43,7 +43,7 @@ export const getOperationItems = (
]
}
if (writeModeTypes && ['string', 'object'].includes(assignedVarType || '')) {
if (writeModeTypes && ['string', 'boolean', 'object'].includes(assignedVarType || '')) {
return writeModeTypes.map(type => ({
value: type,
name: type,

View File

@@ -60,7 +60,6 @@ const useConfig = (id: string, payload: CodeNodeType) => {
})
syncOutputKeyOrders(defaultConfig.outputs)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultConfig])
const handleCodeChange = useCallback((code: string) => {
@@ -85,7 +84,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
}, [allLanguageDefault, inputs, setInputs])
const handleSyncFunctionSignature = useCallback(() => {
const generateSyncSignatureCode = (code: string) => {
const generateSyncSignatureCode = (code: string) => {
let mainDefRe
let newMainDef
if (inputs.code_language === CodeLanguage.javascript) {
@@ -159,7 +158,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
})
const filterVar = useCallback((varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.file, VarType.arrayFile].includes(varPayload.type)
return [VarType.string, VarType.number, VarType.boolean, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayBoolean, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])
const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => {

View File

@@ -38,6 +38,7 @@ import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
@@ -142,12 +143,12 @@ const ConditionItem = ({
const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'
const handleUpdateConditionValue = useCallback((value: string) => {
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
const handleUpdateConditionValue = useCallback((value: string | boolean) => {
if (value === condition.value || (isArrayValue && value === (condition.value as string[])?.[0]))
return
const newCondition = {
...condition,
value: isArrayValue ? [value] : value,
value: isArrayValue ? [value as string] : value,
}
doUpdateCondition(newCondition)
}, [condition, doUpdateCondition, isArrayValue])
@@ -203,8 +204,12 @@ const ConditionItem = ({
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
} = workflowStore.getState()
const resolvedVarType = getVarType({
valueSelector,
conversationVariables,
availableNodes,
isChatMode,
})
@@ -212,7 +217,7 @@ const ConditionItem = ({
const newCondition = produce(condition, (draft) => {
draft.variable_selector = valueSelector
draft.varType = resolvedVarType
draft.value = ''
draft.value = resolvedVarType === VarType.boolean ? false : ''
draft.comparison_operator = getOperators(resolvedVarType)[0]
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
})
@@ -220,6 +225,14 @@ const ConditionItem = ({
setOpen(false)
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey])
const showBooleanInput = useMemo(() => {
if(condition.varType === VarType.boolean)
return true
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if(condition.varType === VarType.arrayBoolean && [ComparisonOperator.contains, ComparisonOperator.notContains].includes(condition.comparison_operator!))
return true
return false
}, [condition])
return (
<div className={cn('mb-1 flex last-of-type:mb-0', className)}>
<div className={cn(
@@ -273,7 +286,7 @@ const ConditionItem = ({
/>
</div>
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && !showBooleanInput && (
<div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'>
<ConditionInput
disabled={disabled}
@@ -285,6 +298,16 @@ const ConditionItem = ({
</div>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && showBooleanInput && (
<div className='p-1'>
<BoolValue
value={condition.value as boolean}
onChange={handleUpdateConditionValue}
/>
</div>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>

View File

@@ -24,7 +24,7 @@ type ConditionValueProps = {
variableSelector: string[]
labelName?: string
operator: ComparisonOperator
value: string | string[]
value: string | string[] | boolean
}
const ConditionValue = ({
variableSelector,
@@ -46,6 +46,9 @@ const ConditionValue = ({
if (Array.isArray(value)) // transfer method
return value[0]
if(value === true || value === false)
return value ? 'True' : 'False'
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
const arr: string[] = b.split('.')
if (isSystemVar(arr))

View File

@@ -1,4 +1,4 @@
import { BlockEnum, type NodeDefault } from '../../types'
import { BlockEnum, type NodeDefault, VarType } 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/blocks'
@@ -58,13 +58,13 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
if (isEmptyRelatedOperator(c.comparison_operator!))
return true
return !!c.value
return (c.varType === VarType.boolean || c.varType === VarType.arrayBoolean) ? c.value === undefined : !!c.value
})
if (!isSet)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
else {
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
if (!isEmptyRelatedOperator(condition.comparison_operator!) && ((condition.varType === VarType.boolean || condition.varType === VarType.arrayBoolean) ? condition.value === undefined : !condition.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
}

View File

@@ -7,6 +7,7 @@ import { isEmptyRelatedOperator } from './utils'
import type { Condition, IfElseNodeType } from './types'
import ConditionValue from './components/condition-value'
import ConditionFilesListValue from './components/condition-files-list-value'
import { VarType } from '../../types'
const i18nPrefix = 'workflow.nodes.ifElse'
const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
@@ -23,18 +24,14 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
if (!c.comparison_operator)
return false
if (isEmptyRelatedOperator(c.comparison_operator!))
return true
return !!c.value
return (c.varType === VarType.boolean || c.varType === VarType.arrayBoolean) ? true : !!c.value
})
return isSet
}
else {
if (isEmptyRelatedOperator(condition.comparison_operator!))
return true
return !!condition.value
return (condition.varType === VarType.boolean || condition.varType === VarType.arrayBoolean) ? true : !!condition.value
}
}, [])
const conditionNotSet = (<div className='flex h-6 items-center space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
@@ -73,7 +70,7 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
<ConditionValue
variableSelector={condition.variable_selector!}
operator={condition.comparison_operator!}
value={condition.value}
value={condition.varType === VarType.boolean ? (!condition.value ? 'False' : condition.value) : condition.value}
/>
)

View File

@@ -41,7 +41,7 @@ export type Condition = {
variable_selector?: ValueSelector
key?: string // sub variable key
comparison_operator?: ComparisonOperator
value: string | string[]
value: string | string[] | boolean
numberVarType?: NumberVarType
sub_variable_condition?: CaseItem
}

View File

@@ -144,7 +144,7 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
varType: varItem.type,
variable_selector: valueSelector,
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
value: '',
value: (varItem.type === VarType.boolean || varItem.type === VarType.arrayBoolean) ? false : '',
})
}
})

View File

@@ -107,6 +107,11 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
case VarType.boolean:
return [
ComparisonOperator.is,
ComparisonOperator.isNot,
]
case VarType.file:
return [
ComparisonOperator.exists,
@@ -114,6 +119,7 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
]
case VarType.arrayString:
case VarType.arrayNumber:
case VarType.arrayBoolean:
return [
ComparisonOperator.contains,
ComparisonOperator.notContains,

View File

@@ -25,7 +25,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
return [VarType.array, VarType.arrayString, VarType.arrayBoolean, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
}, [])
const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {

View File

@@ -9,6 +9,7 @@ import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/u
import SubVariablePicker from './sub-variable-picker'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants'
import { SimpleSelect as Select } from '@/app/components/base/select'
import BoolValue from '../../../panel/chat-variable-panel/components/bool-value'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import cn from '@/utils/classnames'
@@ -28,8 +29,8 @@ const VAR_INPUT_SUPPORTED_KEYS: Record<string, VarType> = {
type Props = {
condition: Condition
onChange: (condition: Condition) => void
varType: VarType
onChange: (condition: Condition) => void
hasSubVariable: boolean
readOnly: boolean
nodeId: string
@@ -58,6 +59,7 @@ const FilterCondition: FC<Props> = ({
const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator)
const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type'
const isBoolean = varType === VarType.boolean
const selectOptions = useMemo(() => {
if (isSelect) {
@@ -112,6 +114,12 @@ const FilterCondition: FC<Props> = ({
/>
)
}
else if (isBoolean) {
inputElement = (<BoolValue
value={condition.value as boolean}
onChange={handleChange('value')}
/>)
}
else if (supportVariableInput) {
inputElement = (
<Input
@@ -162,7 +170,7 @@ const FilterCondition: FC<Props> = ({
<div className='flex space-x-1'>
<ConditionOperator
className='h-8 bg-components-input-bg-normal'
varType={expectedVarType ?? VarType.string}
varType={expectedVarType ?? varType ?? VarType.string}
value={condition.comparison_operator}
onSelect={handleChange('comparison_operator')}
file={hasSubVariable ? { key: condition.key } : undefined}

View File

@@ -38,7 +38,7 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
},
checkValid(payload: ListFilterNodeType, t: any) {
let errorMessages = ''
const { variable, var_type, filter_by } = payload
const { variable, var_type, filter_by, item_var_type } = payload
if (!errorMessages && !variable?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') })
@@ -51,7 +51,7 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
if (!errorMessages && !filter_by.conditions[0]?.comparison_operator)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonOperator') })
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && !filter_by.conditions[0]?.value)
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && (item_var_type === VarType.boolean ? !filter_by.conditions[0]?.value === undefined : !filter_by.conditions[0]?.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') })
}

View File

@@ -14,7 +14,7 @@ export type Limit = {
export type Condition = {
key: string
comparison_operator: ComparisonOperator
value: string | number | string[]
value: string | number | boolean | string[]
}
export type ListFilterNodeType = CommonNodeType & {

View File

@@ -45,7 +45,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
isChatMode,
isConstant: false,
})
let itemVarType = varType
let itemVarType
switch (varType) {
case VarType.arrayNumber:
itemVarType = VarType.number
@@ -59,6 +59,11 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
case VarType.arrayObject:
itemVarType = VarType.object
break
case VarType.arrayBoolean:
itemVarType = VarType.boolean
break
default:
itemVarType = varType
}
return { varType, itemVarType }
}, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, isInIteration, iterationNode, loopNode])
@@ -84,7 +89,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
draft.filter_by.conditions = [{
key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '',
comparison_operator: getOperators(itemVarType, isFileArray ? { key: 'name' } : undefined)[0],
value: '',
value: itemVarType === VarType.boolean ? false : '',
}]
if (isFileArray && draft.order_by.enabled && !draft.order_by.key)
draft.order_by.key = 'name'
@@ -94,7 +99,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
const filterVar = useCallback((varPayload: Var) => {
// Don't know the item struct of VarType.arrayObject, so not support it
return [VarType.arrayNumber, VarType.arrayString, VarType.arrayFile].includes(varPayload.type)
return [VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayFile].includes(varPayload.type)
}, [])
const handleFilterEnabledChange = useCallback((enabled: boolean) => {

View File

@@ -11,7 +11,6 @@ import VisualEditor from './visual-editor'
import SchemaEditor from './schema-editor'
import {
checkJsonSchemaDepth,
convertBooleanToString,
getValidationErrorMessage,
jsonToSchema,
preValidateSchema,
@@ -87,7 +86,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
return
}
convertBooleanToString(schema)
const validationErrors = validateSchemaAgainstDraft7(schema)
if (validationErrors.length > 0) {
setValidationError(getValidationErrorMessage(validationErrors))
@@ -168,7 +166,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
return
}
convertBooleanToString(schema)
const validationErrors = validateSchemaAgainstDraft7(schema)
if (validationErrors.length > 0) {
setValidationError(getValidationErrorMessage(validationErrors))

View File

@@ -39,21 +39,19 @@ type EditCardProps = {
const TYPE_OPTIONS = [
{ value: Type.string, text: 'string' },
{ value: Type.number, text: 'number' },
// { value: Type.boolean, text: 'boolean' },
{ value: Type.boolean, text: 'boolean' },
{ value: Type.object, text: 'object' },
{ value: ArrayType.string, text: 'array[string]' },
{ value: ArrayType.number, text: 'array[number]' },
// { value: ArrayType.boolean, text: 'array[boolean]' },
{ value: ArrayType.object, text: 'array[object]' },
]
const MAXIMUM_DEPTH_TYPE_OPTIONS = [
{ value: Type.string, text: 'string' },
{ value: Type.number, text: 'number' },
// { value: Type.boolean, text: 'boolean' },
{ value: Type.boolean, text: 'boolean' },
{ value: ArrayType.string, text: 'array[string]' },
{ value: ArrayType.number, text: 'array[number]' },
// { value: ArrayType.boolean, text: 'array[boolean]' },
]
const EditCard: FC<EditCardProps> = ({

View File

@@ -303,6 +303,7 @@ export const getValidationErrorMessage = (errors: ValidationError[]) => {
return message
}
// Previous Not support boolean type, so transform boolean to string when paste it into schema editor
export const convertBooleanToString = (schema: any) => {
if (schema.type === Type.boolean)
schema.type = Type.string

View File

@@ -36,6 +36,7 @@ import cn from '@/utils/classnames'
import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import ConditionVarSelector from './condition-var-selector'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
@@ -129,12 +130,12 @@ const ConditionItem = ({
const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'
const handleUpdateConditionValue = useCallback((value: string) => {
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
const handleUpdateConditionValue = useCallback((value: string | boolean) => {
if (value === condition.value || (isArrayValue && value === (condition.value as string[])?.[0]))
return
const newCondition = {
...condition,
value: isArrayValue ? [value] : value,
value: isArrayValue ? [value as string] : value,
}
doUpdateCondition(newCondition)
}, [condition, doUpdateCondition, isArrayValue])
@@ -253,7 +254,7 @@ const ConditionItem = ({
/>
</div>
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && condition.varType !== VarType.boolean && (
<div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'>
<ConditionInput
disabled={disabled}
@@ -264,6 +265,14 @@ const ConditionItem = ({
</div>
)
}
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.boolean
&& <div className='p-1'>
<BoolValue
value={condition.value as boolean}
onChange={handleUpdateConditionValue}
/>
</div>
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>

View File

@@ -18,33 +18,16 @@ import {
ValueType,
VarType,
} from '@/app/components/workflow/types'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
import {
arrayBoolPlaceholder,
arrayNumberPlaceholder,
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
type FormItemProps = {
nodeId: string
@@ -83,6 +66,8 @@ const FormItem = ({
return arrayNumberPlaceholder
if (var_type === VarType.arrayObject)
return arrayObjectPlaceholder
if (var_type === VarType.arrayBoolean)
return arrayBoolPlaceholder
return objectPlaceholder
}, [var_type])
@@ -120,6 +105,14 @@ const FormItem = ({
/>
)
}
{
value_type === ValueType.constant && var_type === VarType.boolean && (
<BoolValue
value={value}
onChange={handleChange}
/>
)
}
{
value_type === ValueType.constant
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
@@ -137,6 +130,15 @@ const FormItem = ({
</div>
)
}
{
value_type === ValueType.constant && var_type === VarType.arrayBoolean && (
<ArrayBoolList
className='mt-2'
list={value || [false]}
onChange={handleChange}
/>
)
}
</div>
)
}

View File

@@ -12,6 +12,7 @@ import type {
} from '@/app/components/workflow/nodes/loop/types'
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
import Toast from '@/app/components/base/toast'
import { ValueType, VarType } from '@/app/components/workflow/types'
type ItemProps = {
item: LoopVariable
@@ -42,12 +43,25 @@ const Item = ({
handleUpdateLoopVariable(item.id, { label: e.target.value })
}, [item.id, handleUpdateLoopVariable])
const getDefaultValue = useCallback((varType: VarType, valueType: ValueType) => {
if(valueType === ValueType.variable)
return undefined
switch (varType) {
case VarType.boolean:
return false
case VarType.arrayBoolean:
return [false]
default:
return undefined
}
}, [])
const handleUpdateItemVarType = useCallback((value: any) => {
handleUpdateLoopVariable(item.id, { var_type: value, value: undefined })
handleUpdateLoopVariable(item.id, { var_type: value, value: getDefaultValue(value, item.value_type) })
}, [item.id, handleUpdateLoopVariable])
const handleUpdateItemValueType = useCallback((value: any) => {
handleUpdateLoopVariable(item.id, { value_type: value, value: undefined })
handleUpdateLoopVariable(item.id, { value_type: value, value: getDefaultValue(item.var_type, value) })
}, [item.id, handleUpdateLoopVariable])
const handleUpdateItemValue = useCallback((value: any) => {

View File

@@ -22,6 +22,10 @@ const VariableTypeSelect = ({
label: 'Object',
value: VarType.object,
},
{
label: 'Boolean',
value: VarType.boolean,
},
{
label: 'Array[string]',
value: VarType.arrayString,
@@ -34,6 +38,10 @@ const VariableTypeSelect = ({
label: 'Array[object]',
value: VarType.arrayObject,
},
{
label: 'Array[boolean]',
value: VarType.arrayBoolean,
},
]
return (

View File

@@ -1,4 +1,4 @@
import { BlockEnum } from '../../types'
import { BlockEnum, VarType } from '../../types'
import type { NodeDefault } from '../../types'
import { ComparisonOperator, LogicalOperator, type LoopNodeType } from './types'
import { isEmptyRelatedOperator } from './utils'
@@ -55,7 +55,7 @@ const nodeDefault: NodeDefault<LoopNodeType> = {
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
else {
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
if (!isEmptyRelatedOperator(condition.comparison_operator!) && (condition.varType === VarType.boolean ? condition.value === undefined : !condition.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
}
}

View File

@@ -44,7 +44,7 @@ export type Condition = {
variable_selector?: ValueSelector
key?: string // sub variable key
comparison_operator?: ComparisonOperator
value: string | string[]
value: string | string[] | boolean
numberVarType?: NumberVarType
sub_variable_condition?: CaseItem
}

View File

@@ -63,7 +63,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
varType: varItem.type,
variable_selector: valueSelector,
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
value: '',
value: varItem.type === VarType.boolean ? 'false' : '',
})
})
setInputs(newInputs)

View File

@@ -107,7 +107,7 @@ const useSingleRunFormParams = ({
}, [runResult, loopRunResult, t])
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
@@ -149,16 +149,15 @@ const useSingleRunFormParams = ({
})
payload.loop_variables?.forEach((loopVariable) => {
if(loopVariable.value_type === ValueType.variable)
if (loopVariable.value_type === ValueType.variable)
allInputs.push(loopVariable.value)
})
const inputVarsFromValue: InputVar[] = []
const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue]
const existVarsKey: Record<string, boolean> = {}
const uniqueVarInputs: InputVar[] = []
varInputs.forEach((input) => {
if(!input)
if (!input)
return
if (!existVarsKey[input.variable]) {
existVarsKey[input.variable] = true
@@ -191,7 +190,7 @@ const useSingleRunFormParams = ({
if (condition.variable_selector)
vars.push(condition.variable_selector)
if(condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length)
vars.push(...getVarFromCaseItem(condition.sub_variable_condition))
return vars
}
@@ -203,7 +202,7 @@ const useSingleRunFormParams = ({
vars.push(...conditionVars)
})
payload.loop_variables?.forEach((loopVariable) => {
if(loopVariable.value_type === ValueType.variable)
if (loopVariable.value_type === ValueType.variable)
vars.push(loopVariable.value)
})
const hasFilterLoopVars = vars.filter(item => item[0] !== id)

View File

@@ -107,6 +107,13 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
case VarType.boolean:
return [
ComparisonOperator.is,
ComparisonOperator.isNot,
ComparisonOperator.empty,
ComparisonOperator.notEmpty,
]
case VarType.object:
return [
ComparisonOperator.empty,

View File

@@ -35,7 +35,7 @@ type Props = {
onCancel?: () => void
}
const TYPES = [ParamType.string, ParamType.number, ParamType.arrayString, ParamType.arrayNumber, ParamType.arrayObject]
const TYPES = [ParamType.string, ParamType.number, ParamType.bool, ParamType.arrayString, ParamType.arrayNumber, ParamType.arrayObject, ParamType.arrayBool]
const AddExtractParameter: FC<Props> = ({
type,

View File

@@ -3,11 +3,12 @@ import type { CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting
export enum ParamType {
string = 'string',
number = 'number',
bool = 'bool',
bool = 'boolean',
select = 'select',
arrayString = 'array[string]',
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
arrayBool = 'array[boolean]',
}
export type Param = {

View File

@@ -19,7 +19,7 @@ type Props = {
className?: string
readonly: boolean
payload: InputVar
onChange?: (item: InputVar, moreInfo?: MoreInfo) => void
onChange?: (item: InputVar, moreInfo?: MoreInfo) => boolean
onRemove?: () => void
rightContent?: React.JSX.Element
varKeys?: string[]
@@ -31,7 +31,7 @@ const VarItem: FC<Props> = ({
className,
readonly,
payload,
onChange = noop,
onChange = () => true,
onRemove = noop,
rightContent,
varKeys = [],
@@ -48,7 +48,9 @@ const VarItem: FC<Props> = ({
}] = useBoolean(false)
const handlePayloadChange = useCallback((payload: InputVar, moreInfo?: MoreInfo) => {
onChange(payload, moreInfo)
const isValid = onChange(payload, moreInfo)
if(!isValid)
return
hideEditVarModal()
}, [onChange, hideEditVarModal])
return (

View File

@@ -9,6 +9,8 @@ import { v4 as uuid4 } from 'uuid'
import { ReactSortable } from 'react-sortablejs'
import { RiDraggable } from '@remixicon/react'
import cn from '@/utils/classnames'
import { hasDuplicateStr } from '@/utils/var'
import Toast from '@/app/components/base/toast'
type Props = {
readonly: boolean
@@ -28,7 +30,26 @@ const VarList: FC<Props> = ({
const newList = produce(list, (draft) => {
draft[index] = payload
})
let errorMsgKey = ''
let typeName = ''
if (hasDuplicateStr(newList.map(item => item.variable))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.varName'
}
else if (hasDuplicateStr(newList.map(item => item.label as string))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.labelName'
}
if (errorMsgKey) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { key: t(typeName) }),
})
return false
}
onChange(newList, moreInfo ? { index, payload: moreInfo } : undefined)
return true
}
}, [list, onChange])

View File

@@ -34,7 +34,8 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
} = useConfig(id, data)
const handleAddVarConfirm = (payload: InputVar) => {
handleAddVariable(payload)
const isValid = handleAddVariable(payload)
if (!isValid) return
hideAddVarModal()
}

View File

@@ -11,8 +11,12 @@ import {
useWorkflow,
} from '@/app/components/workflow/hooks'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { hasDuplicateStr } from '@/utils/var'
import Toast from '@/app/components/base/toast'
import { useTranslation } from 'react-i18next'
const useConfig = (id: string, payload: StartNodeType) => {
const { t } = useTranslation()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
const isChatMode = useIsChatMode()
@@ -80,7 +84,27 @@ const useConfig = (id: string, payload: StartNodeType) => {
const newInputs = produce(inputs, (draft: StartNodeType) => {
draft.variables.push(payload)
})
const newList = newInputs.variables
let errorMsgKey = ''
let typeName = ''
if(hasDuplicateStr(newList.map(item => item.variable))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.varName'
}
else if(hasDuplicateStr(newList.map(item => item.label as string))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.labelName'
}
if (errorMsgKey) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { key: t(typeName) }),
})
return false
}
setInputs(newInputs)
return true
}, [inputs, setInputs])
return {
readOnly,

View File

@@ -65,7 +65,6 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
...defaultConfig,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultConfig])
const handleCodeChange = useCallback((template: string) => {
@@ -76,7 +75,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
}, [setInputs])
const filterVar = useCallback((varPayload: Var) => {
return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type)
return [VarType.string, VarType.number, VarType.boolean, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayObject].includes(varPayload.type)
}, [])
return {

View File

@@ -132,7 +132,6 @@ export const useGetAvailableVars = () => {
if (!currentNode)
return []
const beforeNodes = getBeforeNodesInSameBranchIncludeParent(nodeId)
availableNodes.push(...beforeNodes)
const parentNode = nodes.find(node => node.id === currentNode.parentId)
@@ -143,7 +142,7 @@ export const useGetAvailableVars = () => {
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
isChatMode,
hideEnv,
hideChatVar: hideEnv,
hideChatVar: false,
filterVar,
})
.map(node => ({

View File

@@ -0,0 +1,72 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import produce from 'immer'
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
import Button from '@/app/components/base/button'
import BoolValue from './bool-value'
import cn from '@/utils/classnames'
type Props = {
className?: string
list: boolean[]
onChange: (list: boolean[]) => void
}
const ArrayValueList: FC<Props> = ({
className,
list,
onChange,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((index: number) => {
return (value: boolean) => {
const newList = produce(list, (draft: any[]) => {
draft[index] = value
})
onChange(newList)
}
}, [list, onChange])
const handleItemRemove = useCallback((index: number) => {
return () => {
const newList = produce(list, (draft) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange])
const handleItemAdd = useCallback(() => {
const newList = produce(list, (draft: any[]) => {
draft.push(false)
})
onChange(newList)
}, [list, onChange])
return (
<div className={cn('w-full space-y-2', className)}>
{list.map((item, index) => (
<div className='flex items-center space-x-1' key={index}>
<BoolValue
value={item}
onChange={handleChange(index)}
/>
<RemoveButton
className='!bg-gray-100 !p-2 hover:!bg-gray-200'
onClick={handleItemRemove(index)}
/>
</div>
))}
<Button variant='tertiary' className='w-full' onClick={handleItemAdd}>
<RiAddLine className='mr-1 h-4 w-4' />
<span>{t('workflow.chatVariable.modal.addArrayValue')}</span>
</Button>
</div>
)
}
export default React.memo(ArrayValueList)

View File

@@ -0,0 +1,37 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import OptionCard from '../../../nodes/_base/components/option-card'
type Props = {
value: boolean
onChange: (value: boolean) => void
}
const BoolValue: FC<Props> = ({
value,
onChange,
}) => {
const booleanValue = value
const handleChange = useCallback((newValue: boolean) => {
return () => {
onChange(newValue)
}
}, [onChange])
return (
<div className='flex w-full space-x-1'>
<OptionCard className='grow'
selected={booleanValue}
title='True'
onSelect={handleChange(true)}
/>
<OptionCard className='grow'
selected={!booleanValue}
title='False'
onSelect={handleChange(false)}
/>
</div>
)
}
export default React.memo(BoolValue)

View File

@@ -16,6 +16,15 @@ import type { ConversationVariable } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import cn from '@/utils/classnames'
import BoolValue from './bool-value'
import ArrayBoolList from './array-bool-list'
import {
arrayBoolPlaceholder,
arrayNumberPlaceholder,
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
export type ModalPropsType = {
@@ -33,39 +42,14 @@ type ObjectValueItem = {
const typeList = [
ChatVarType.String,
ChatVarType.Number,
ChatVarType.Boolean,
ChatVarType.Object,
ChatVarType.ArrayString,
ChatVarType.ArrayNumber,
ChatVarType.ArrayBoolean,
ChatVarType.ArrayObject,
]
const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
const ChatVariableModal = ({
chatVar,
onClose,
@@ -94,6 +78,8 @@ const ChatVariableModal = ({
return arrayNumberPlaceholder
if (type === ChatVarType.ArrayObject)
return arrayObjectPlaceholder
if (type === ChatVarType.ArrayBoolean)
return arrayBoolPlaceholder
return objectPlaceholder
}, [type])
const getObjectValue = useCallback(() => {
@@ -122,12 +108,16 @@ const ChatVariableModal = ({
return value || ''
case ChatVarType.Number:
return value || 0
case ChatVarType.Boolean:
return value === undefined ? true : value
case ChatVarType.Object:
return editInJSON ? value : formatValueFromObject(objectValue)
case ChatVarType.ArrayString:
case ChatVarType.ArrayNumber:
case ChatVarType.ArrayObject:
return value?.filter(Boolean) || []
case ChatVarType.ArrayBoolean:
return value || []
}
}
@@ -157,6 +147,10 @@ const ChatVariableModal = ({
setEditInJSON(true)
if (v === ChatVarType.String || v === ChatVarType.Number || v === ChatVarType.Object)
setEditInJSON(false)
if(v === ChatVarType.Boolean)
setValue(false)
if (v === ChatVarType.ArrayBoolean)
setValue([false])
setType(v)
}
@@ -202,6 +196,11 @@ const ChatVariableModal = ({
setValue(value?.length ? value : [undefined])
}
}
if(type === ChatVarType.ArrayBoolean) {
if(editInJSON)
setEditorContent(JSON.stringify(value.map((item: boolean) => item ? 'True' : 'False')))
}
setEditInJSON(editInJSON)
}
@@ -213,7 +212,16 @@ const ChatVariableModal = ({
else {
setEditorContent(content)
try {
const newValue = JSON.parse(content)
let newValue = JSON.parse(content)
if(type === ChatVarType.ArrayBoolean) {
newValue = newValue.map((item: string | boolean) => {
if (item === 'True' || item === 'true' || item === true)
return true
if (item === 'False' || item === 'false' || item === false)
return false
return undefined
}).filter((item?: boolean) => item !== undefined)
}
setValue(newValue)
}
catch {
@@ -304,7 +312,7 @@ const ChatVariableModal = ({
<div className='mb-4'>
<div className='system-sm-semibold mb-1 flex h-6 items-center justify-between text-text-secondary'>
<div>{t('workflow.chatVariable.modal.value')}</div>
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) && (
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber || type === ChatVarType.ArrayBoolean) && (
<Button
variant='ghost'
size='small'
@@ -345,6 +353,12 @@ const ChatVariableModal = ({
type='number'
/>
)}
{type === ChatVarType.Boolean && (
<BoolValue
value={value}
onChange={setValue}
/>
)}
{type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
@@ -365,6 +379,13 @@ const ChatVariableModal = ({
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayBoolean && !editInJSON && (
<ArrayBoolList
list={value || [true]}
onChange={setValue}
/>
)}
{editInJSON && (
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
<CodeEditor

View File

@@ -1,8 +1,10 @@
export enum ChatVarType {
Number = 'number',
String = 'string',
Boolean = 'boolean',
Object = 'object',
ArrayString = 'array[string]',
ArrayNumber = 'array[number]',
ArrayBoolean = 'array[boolean]',
ArrayObject = 'array[object]',
}

View File

@@ -0,0 +1,35 @@
export const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
export const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
export const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
export const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
export const arrayBoolPlaceholder = `# example
# [
# "True",
# "False"
# ]`

View File

@@ -180,9 +180,11 @@ export enum InputVarType {
paragraph = 'paragraph',
select = 'select',
number = 'number',
checkbox = 'checkbox',
url = 'url',
files = 'files',
json = 'json', // obj, array
jsonObject = 'json_object', // only object support define json schema
contexts = 'contexts', // knowledge retrieval
iterator = 'iterator', // iteration input
singleFile = 'file',
@@ -208,6 +210,7 @@ export type InputVar = {
getVarValueFromDependent?: boolean
hide?: boolean
isFileItem?: boolean
json_schema?: string // for jsonObject type
} & Partial<UploadFileSetting>
export type ModelConfig = {
@@ -266,6 +269,7 @@ export enum VarType {
arrayString = 'array[string]',
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
arrayBoolean = 'array[boolean]',
arrayFile = 'array[file]',
any = 'any',
arrayAny = 'array[any]',

View File

@@ -21,6 +21,7 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { VarInInspect } from '@/types/workflow'
import { VarInInspectType } from '@/types/workflow'
import cn from '@/utils/classnames'
import BoolValue from '../panel/chat-variable-panel/components/bool-value'
type Props = {
currentVar: VarInInspect
@@ -35,6 +36,8 @@ const ValueContent = ({
const errorMessageRef = useRef<HTMLDivElement>(null)
const [editorHeight, setEditorHeight] = useState(0)
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
const showBoolEditor = typeof currentVar.value === 'boolean'
const showBoolArrayEditor = Array.isArray(currentVar.value) && currentVar.value.every(v => typeof v === 'boolean')
const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
const showJSONEditor = !isSysFiles && (currentVar.value_type === 'object' || currentVar.value_type === 'array[string]' || currentVar.value_type === 'array[number]' || currentVar.value_type === 'array[object]' || currentVar.value_type === 'array[any]')
const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
@@ -72,7 +75,6 @@ const ValueContent = ({
if (showFileEditor)
setFileValue(formatFileValue(currentVar))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentVar.id, currentVar.value])
const handleTextChange = (value: string) => {
@@ -178,6 +180,35 @@ const ValueContent = ({
onChange={e => handleTextChange(e.target.value)}
/>
)}
{showBoolEditor && (
<div className='w-[295px]'>
<BoolValue
value={currentVar.value as boolean}
onChange={(newValue) => {
setValue(newValue)
debounceValueChange(currentVar.id, newValue)
}}
/>
</div>
)}
{
showBoolArrayEditor && (
<div className='w-[295px] space-y-1'>
{currentVar.value.map((v: boolean, i: number) => (
<BoolValue
key={i}
value={v}
onChange={(newValue) => {
const newArray = [...(currentVar.value as boolean[])]
newArray[i] = newValue
setValue(newArray)
debounceValueChange(currentVar.id, newArray)
}}
/>
))}
</div>
)
}
{showJSONEditor && (
<SchemaEditor
readonly={JSONEditorDisabled}

View File

@@ -374,6 +374,10 @@ const translation = {
'paragraph': 'Paragraph',
'select': 'Select',
'number': 'Number',
'checkbox': 'Checkbox',
'json': 'JSON Code',
'jsonSchema': 'JSON Schema',
'optional': 'optional',
'single-file': 'Single File',
'multi-files': 'File List',
'notSet': 'Not set, try typing {{input}} in the prefix prompt',

View File

@@ -372,8 +372,12 @@ const translation = {
'paragraph': '段落',
'select': '下拉选项',
'number': '数字',
'checkbox': '复选框',
'single-file': '单文件',
'multi-files': '文件列表',
'json': 'JSON',
'jsonSchema': 'JSON Schema',
'optional': '可选',
'notSet': '未设置,在 Prompt 中输入 {{input}} 试试',
'stringTitle': '文本框设置',
'maxLength': '最大长度',

View File

@@ -9,7 +9,7 @@ import type {
MetadataFilteringModeEnum,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { ModelConfig as NodeModelConfig } from '@/app/components/workflow/types'
export type Inputs = Record<string, string | number | object>
export type Inputs = Record<string, string | number | object | boolean>
export enum PromptMode {
simple = 'simple',
@@ -60,6 +60,7 @@ export type PromptVariable = {
icon?: string
icon_background?: string
hide?: boolean // used in frontend to hide variable
json_schema?: string
}
export type CompletionParams = {

View File

@@ -18,6 +18,9 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
if (item.number)
return ['number', item.number]
if (item.checkbox)
return ['boolean', item.checkbox]
if (item.file)
return ['file', item.file]
@@ -27,6 +30,9 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
if (item.external_data_tool)
return [item.external_data_tool.type, item.external_data_tool]
if (item.json_object)
return ['json_object', item.json_object]
return ['select', item.select || {}]
})()
const is_context_var = dataset_query_variable === content?.variable
@@ -135,9 +141,9 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
} as any)
return
}
if (item.type === 'number') {
if (item.type === 'number' || item.type === 'checkbox') {
userInputs.push({
number: {
[item.type]: {
label: item.name,
variable: item.key,
required: item.required !== false, // default true
@@ -177,3 +183,17 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
return userInputs
}
export const formatBooleanInputs = (useInputs?: PromptVariable[] | null, inputs?: Record<string, string | number | object | boolean> | null) => {
if(!useInputs)
return inputs
const res = { ...(inputs || {}) }
useInputs.forEach((item) => {
const isBooleanInput = item.type === 'boolean'
if (isBooleanInput) {
// Convert boolean inputs to boolean type
res[item.key] = !!res[item.key]
}
})
return res
}

View File

@@ -45,7 +45,7 @@ export const getNewVarInWorkflow = (key: string, type = InputVarType.textInput)
}
}
export const checkKey = (key: string, canBeEmpty?: boolean) => {
export const checkKey = (key: string, canBeEmpty?: boolean, keys?: string[]) => {
if (key.length === 0 && !canBeEmpty)
return 'canNoBeEmpty'
@@ -82,6 +82,17 @@ export const checkKeys = (keys: string[], canBeEmpty?: boolean) => {
return { isValid, errorKey, errorMessageKey }
}
export const hasDuplicateStr = (strArr: string[]) => {
const strObj: Record<string, number> = {}
strArr.forEach((str) => {
if (strObj[str])
strObj[str] += 1
else
strObj[str] = 1
})
return !!Object.keys(strObj).find(key => strObj[key] > 1)
}
const varRegex = /\{\{([a-zA-Z_]\w*)\}\}/g
export const getVars = (value: string) => {
if (!value)