mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
merge main
This commit is contained in:
@ -0,0 +1,20 @@
|
||||
import { memo } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
const GlobalVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const setShowPanel = useStore(s => s.setShowGlobalVariablePanel)
|
||||
|
||||
const handleClick = () => {
|
||||
setShowPanel(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className='p-2' disabled={disabled} onClick={handleClick}>
|
||||
<GlobalVariable className='w-4 h-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(GlobalVariableButton)
|
||||
88
web/app/components/workflow/hooks/use-config-vision.ts
Normal file
88
web/app/components/workflow/hooks/use-config-vision.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import produce from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import type { ModelConfig, VisionSetting } from '@/app/components/workflow/types'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
||||
type Payload = {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
|
||||
type Params = {
|
||||
payload: Payload
|
||||
onChange: (payload: Payload) => void
|
||||
}
|
||||
const useConfigVision = (model: ModelConfig, {
|
||||
payload = {
|
||||
enabled: false,
|
||||
},
|
||||
onChange,
|
||||
}: Params) => {
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: model.provider,
|
||||
model: model.name,
|
||||
},
|
||||
)
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const getIsVisionModel = useCallback(() => {
|
||||
return !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
}, [currModel])
|
||||
|
||||
const isVisionModel = getIsVisionModel()
|
||||
|
||||
const handleVisionResolutionEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.enabled = enabled
|
||||
if (enabled && isChatMode) {
|
||||
draft.configs = {
|
||||
detail: Resolution.high,
|
||||
variable_selector: ['sys', 'files'],
|
||||
}
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [isChatMode, onChange, payload])
|
||||
|
||||
const handleVisionResolutionChange = useCallback((config: VisionSetting) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.configs = config
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleModelChanged = useCallback(() => {
|
||||
const isVisionModel = getIsVisionModel()
|
||||
if (!isVisionModel) {
|
||||
handleVisionResolutionEnabledChange(false)
|
||||
return
|
||||
}
|
||||
if (payload.enabled) {
|
||||
onChange({
|
||||
enabled: true,
|
||||
configs: {
|
||||
detail: Resolution.high,
|
||||
variable_selector: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [getIsVisionModel, handleVisionResolutionEnabledChange, onChange, payload.enabled])
|
||||
|
||||
return {
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
handleModelChanged,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfigVision
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import type { CodeLanguage } from '../../code/types'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
import { AppType } from '@/types/app'
|
||||
import type { CodeGenRes } from '@/service/debug'
|
||||
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
onGenerated?: (prompt: string) => void
|
||||
codeLanguages: CodeLanguage
|
||||
}
|
||||
|
||||
const CodeGenerateBtn: FC<Props> = ({
|
||||
className,
|
||||
codeLanguages,
|
||||
onGenerated,
|
||||
}) => {
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
const handleAutomaticRes = useCallback((res: CodeGenRes) => {
|
||||
onGenerated?.(res.code)
|
||||
showAutomaticFalse()
|
||||
}, [onGenerated, showAutomaticFalse])
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<ActionButton
|
||||
className='hover:bg-[#155EFF]/8'
|
||||
onClick={showAutomaticTrue}>
|
||||
<Generator className='w-4 h-4 text-primary-600' />
|
||||
</ActionButton>
|
||||
{showAutomatic && (
|
||||
<GetCodeGeneratorResModal
|
||||
mode={AppType.chat}
|
||||
isShow={showAutomatic}
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeGenerateBtn)
|
||||
@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import VarReferencePicker from './variable/var-reference-picker'
|
||||
import ResolutionPicker from '@/app/components/workflow/nodes/llm/components/resolution-picker'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { type ValueSelector, type Var, VarType, type VisionSetting } from '@/app/components/workflow/types'
|
||||
import { Resolution } from '@/types/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type Props = {
|
||||
isVisionModel: boolean
|
||||
readOnly: boolean
|
||||
enabled: boolean
|
||||
onEnabledChange: (enabled: boolean) => void
|
||||
nodeId: string
|
||||
config?: VisionSetting
|
||||
onConfigChange: (config: VisionSetting) => void
|
||||
}
|
||||
|
||||
const ConfigVision: FC<Props> = ({
|
||||
isVisionModel,
|
||||
readOnly,
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
nodeId,
|
||||
config = {
|
||||
detail: Resolution.high,
|
||||
variable_selector: [],
|
||||
},
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filterVar = useCallback((payload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(payload.type)
|
||||
}, [])
|
||||
const handleVisionResolutionChange = useCallback((resolution: Resolution) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.detail = resolution
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
const handleVarSelectorChange = useCallback((valueSelector: ValueSelector | string) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.variable_selector = valueSelector as ValueSelector
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
return (
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.vision`)}
|
||||
tooltip={t('appDebug.vision.description')!}
|
||||
operations={
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.vision.onlySupportVisionModelTip')!}
|
||||
disabled={isVisionModel}
|
||||
>
|
||||
<Switch disabled={readOnly || !isVisionModel} size='md' defaultValue={!isVisionModel ? false : enabled} onChange={onEnabledChange} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(enabled && isVisionModel)
|
||||
? (
|
||||
<div>
|
||||
<VarReferencePicker
|
||||
className='mb-4'
|
||||
filterVar={filterVar}
|
||||
nodeId={nodeId}
|
||||
value={config.variable_selector || []}
|
||||
onChange={handleVarSelectorChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
<ResolutionPicker
|
||||
value={config.detail}
|
||||
onChange={handleVisionResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigVision)
|
||||
@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { FileTypeIcon } from '@/app/components/base/file-uploader'
|
||||
|
||||
type Props = {
|
||||
type: SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video | SupportUploadFileTypes.custom
|
||||
selected: boolean
|
||||
onToggle: (type: SupportUploadFileTypes) => void
|
||||
onCustomFileTypesChange?: (customFileTypes: string[]) => void
|
||||
customFileTypes?: string[]
|
||||
}
|
||||
|
||||
const FileTypeItem: FC<Props> = ({
|
||||
type,
|
||||
selected,
|
||||
onToggle,
|
||||
customFileTypes = [],
|
||||
onCustomFileTypesChange = () => { },
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnSelect = useCallback(() => {
|
||||
onToggle(type)
|
||||
}, [onToggle, type])
|
||||
|
||||
const isCustomSelected = type === SupportUploadFileTypes.custom && selected
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg bg-components-option-card-option-bg border border-components-option-card-option-border cursor-pointer select-none',
|
||||
!isCustomSelected && 'py-2 px-3',
|
||||
selected && 'border-[1.5px] bg-components-option-card-option-selected-bg border-components-option-card-option-selected-border',
|
||||
!selected && 'hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
|
||||
)}
|
||||
onClick={handleOnSelect}
|
||||
>
|
||||
{isCustomSelected
|
||||
? (
|
||||
<div>
|
||||
<div className='flex items-center p-3 pb-2 border-b border-divider-subtle'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<div className='mx-2 grow text-text-primary system-sm-medium'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
<div className='p-3' onClick={e => e.stopPropagation()}>
|
||||
<TagInput
|
||||
items={customFileTypes}
|
||||
onChange={onCustomFileTypesChange}
|
||||
placeholder={t('appDebug.variableConfig.file.custom.createPlaceholder')!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<div className='mx-2 grow'>
|
||||
<div className='text-text-primary system-sm-medium'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<div className='mt-1 text-text-tertiary system-2xs-regular-uppercase'>{type !== SupportUploadFileTypes.custom ? FILE_EXTS[type].join(', ') : t('appDebug.variableConfig.file.custom.description')}</div>
|
||||
</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FileTypeItem)
|
||||
@ -0,0 +1,195 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { UploadFileSetting } from '../../../types'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import OptionCard from './option-card'
|
||||
import FileTypeItem from './file-type-item'
|
||||
import InputNumberWithSlider from './input-number-with-slider'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
|
||||
type Props = {
|
||||
payload: UploadFileSetting
|
||||
isMultiple: boolean
|
||||
inFeaturePanel?: boolean
|
||||
hideSupportFileType?: boolean
|
||||
onChange: (payload: UploadFileSetting) => void
|
||||
}
|
||||
|
||||
const FileUploadSetting: FC<Props> = ({
|
||||
payload,
|
||||
isMultiple,
|
||||
inFeaturePanel = false,
|
||||
hideSupportFileType = false,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
allowed_file_upload_methods,
|
||||
max_length,
|
||||
allowed_file_types,
|
||||
allowed_file_extensions,
|
||||
} = payload
|
||||
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
|
||||
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (type === SupportUploadFileTypes.custom) {
|
||||
if (!draft.allowed_file_types.includes(SupportUploadFileTypes.custom))
|
||||
draft.allowed_file_types = [SupportUploadFileTypes.custom]
|
||||
|
||||
else
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
}
|
||||
else {
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== SupportUploadFileTypes.custom)
|
||||
if (draft.allowed_file_types.includes(type))
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
else
|
||||
draft.allowed_file_types.push(type)
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleUploadMethodChange = useCallback((method: TransferMethod) => {
|
||||
return () => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (method === TransferMethod.all)
|
||||
draft.allowed_file_upload_methods = [TransferMethod.local_file, TransferMethod.remote_url]
|
||||
else
|
||||
draft.allowed_file_upload_methods = [method]
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleCustomFileTypesChange = useCallback((customFileTypes: string[]) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.allowed_file_extensions = customFileTypes.map((v) => {
|
||||
if (v.startsWith('.')) // Not start with dot
|
||||
return v.slice(1)
|
||||
return v
|
||||
})
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleMaxUploadNumLimitChange = useCallback((value: number) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.max_length = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!inFeaturePanel && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions?.map(item => `.${item}`)}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.uploadFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.localUpload')}
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.local_file)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.local_file)}
|
||||
/>
|
||||
<OptionCard
|
||||
title="URL"
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.remote_url)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.both')}
|
||||
selected={allowed_file_upload_methods.includes(TransferMethod.local_file) && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.all)}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
{isMultiple && (
|
||||
<Field
|
||||
className='mt-4'
|
||||
title={t('appDebug.variableConfig.maxNumberOfUploads')!}
|
||||
>
|
||||
<div>
|
||||
<div className='mb-1.5 text-text-tertiary body-xs-regular'>{t('appDebug.variableConfig.maxNumberTip', {
|
||||
imgLimit: formatFileSize(imgSizeLimit),
|
||||
docLimit: formatFileSize(docSizeLimit),
|
||||
audioLimit: formatFileSize(audioSizeLimit),
|
||||
videoLimit: formatFileSize(videoSizeLimit),
|
||||
})}</div>
|
||||
|
||||
<InputNumberWithSlider
|
||||
value={max_length}
|
||||
min={1}
|
||||
max={10}
|
||||
onChange={handleMaxUploadNumLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
{inFeaturePanel && !hideSupportFileType && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FileUploadSetting)
|
||||
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
type Props = {
|
||||
value: number
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
max?: number
|
||||
readonly?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputNumberWithSlider: FC<Props> = ({
|
||||
value,
|
||||
defaultValue = 0,
|
||||
min,
|
||||
max,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleBlur = useCallback(() => {
|
||||
if (value === undefined || value === null) {
|
||||
onChange(defaultValue)
|
||||
return
|
||||
}
|
||||
if (max !== undefined && value > max) {
|
||||
onChange(max)
|
||||
return
|
||||
}
|
||||
if (min !== undefined && value < min)
|
||||
onChange(min)
|
||||
}, [defaultValue, max, min, onChange, value])
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(Number.parseFloat(e.target.value))
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className='flex justify-between items-center h-8 space-x-2'>
|
||||
<input
|
||||
value={value}
|
||||
className='shrink-0 block pl-3 w-12 h-8 appearance-none outline-none rounded-lg bg-components-input-bg-normal text-[13px] text-components-input-text-filled'
|
||||
type='number'
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Slider
|
||||
className='grow'
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={onChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputNumberWithSlider)
|
||||
85
web/app/components/workflow/nodes/code/dependency-picker.tsx
Normal file
85
web/app/components/workflow/nodes/code/dependency-picker.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import type { CodeDependency } from './types'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
value: CodeDependency
|
||||
available_dependencies: CodeDependency[]
|
||||
onChange: (dependency: CodeDependency) => void
|
||||
}
|
||||
|
||||
const DependencyPicker: FC<Props> = ({
|
||||
available_dependencies,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const handleChange = useCallback((dependency: CodeDependency) => {
|
||||
return () => {
|
||||
setOpen(false)
|
||||
onChange(dependency)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='flex-grow cursor-pointer'>
|
||||
<div className='flex items-center h-8 justify-between px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px]'>
|
||||
<div className='grow w-0 truncate' title={value.name}>{value.name}</div>
|
||||
<RiArrowDownSLine className='shrink-0 w-3.5 h-3.5 text-gray-700' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
<div className='p-1 bg-white rounded-lg shadow-sm' style={{
|
||||
width: 350,
|
||||
}}>
|
||||
<div className='mb-2 mx-1'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchText}
|
||||
placeholder={t('workflow.nodes.code.searchDependencies') || ''}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-[30vh] overflow-y-auto'>
|
||||
{available_dependencies.filter((v) => {
|
||||
if (!searchText)
|
||||
return true
|
||||
return v.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
}).map(dependency => (
|
||||
<div
|
||||
key={dependency.name}
|
||||
className='flex items-center h-[30px] justify-between pl-3 pr-2 rounded-lg hover:bg-gray-100 text-gray-900 text-[13px] cursor-pointer'
|
||||
onClick={handleChange(dependency)}
|
||||
>
|
||||
<div className='w-0 grow truncate'>{dependency.name}</div>
|
||||
{dependency.name === value.name && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DependencyPicker)
|
||||
@ -0,0 +1,36 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<DocExtractorNodeType> = {
|
||||
defaultValue: {
|
||||
variable_selector: [],
|
||||
is_array_file: false,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: DocExtractorNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable_selector: variable } = payload
|
||||
|
||||
if (!errorMessages && !variable?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.assignedVariable') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
@ -0,0 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.docExtractor'
|
||||
|
||||
const NodeComponent: FC<NodeProps<DocExtractorNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
const { variable_selector: variable } = data
|
||||
|
||||
if (!variable || variable.length === 0)
|
||||
return null
|
||||
|
||||
const isSystem = isSystemVar(variable)
|
||||
const isEnv = isENV(variable)
|
||||
const isChatVar = isConversationVar(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
return (
|
||||
<div className='relative px-3'>
|
||||
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.inputVar`)}</div>
|
||||
<NodeVariableItem
|
||||
node={node as Node}
|
||||
isEnv={isEnv}
|
||||
isChatVar={isChatVar}
|
||||
varName={varName}
|
||||
className='bg-workflow-block-parma-bg'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NodeComponent)
|
||||
@ -0,0 +1,88 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import Split from '../_base/components/split'
|
||||
import { useNodeHelpLink } from '../_base/hooks/use-node-help-link'
|
||||
import useConfig from './use-config'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import { fetchSupportFileTypes } from '@/service/datasets'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { BlockEnum, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.docExtractor'
|
||||
|
||||
const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const link = useNodeHelpLink(BlockEnum.DocExtractor)
|
||||
const { data: supportFileTypesResponse } = useSWR({ url: '/files/support-type' }, fetchSupportFileTypes)
|
||||
const supportTypes = supportFileTypesResponse?.allowed_extensions || []
|
||||
const supportTypesShowNames = (() => {
|
||||
const extensionMap: { [key: string]: string } = {
|
||||
md: 'markdown',
|
||||
pptx: 'pptx',
|
||||
htm: 'html',
|
||||
xlsx: 'xlsx',
|
||||
docx: 'docx',
|
||||
}
|
||||
|
||||
return [...supportTypes]
|
||||
.map(item => extensionMap[item] || item) // map to standardized extension
|
||||
.map(item => item.toLowerCase()) // convert to lower case
|
||||
.filter((item, index, self) => self.indexOf(item) === index) // remove duplicates
|
||||
.join(locale !== LanguagesSupported[1] ? ', ' : '、 ')
|
||||
})()
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarChanges,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.variable_selector || []}
|
||||
onChange={handleVarChanges}
|
||||
filterVar={filterVar}
|
||||
typePlaceHolder='File | Array[File]'
|
||||
/>
|
||||
<div className='mt-1 py-0.5 text-text-tertiary body-xs-regular'>
|
||||
{t(`${i18nPrefix}.supportFileTypes`, { types: supportTypesShowNames })}
|
||||
<a className='text-text-accent' href={link} target='_blank'>{t(`${i18nPrefix}.learnMore`)}</a>
|
||||
</div>
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<VarItem
|
||||
name='text'
|
||||
type={inputs.is_array_file ? 'array[string]' : 'string'}
|
||||
description={t(`${i18nPrefix}.outputVars.text`)}
|
||||
/>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
@ -0,0 +1,6 @@
|
||||
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export type DocExtractorNodeType = CommonNodeType & {
|
||||
variable_selector: ValueSelector
|
||||
is_array_file: boolean
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: DocExtractorNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<DocExtractorNodeType>(id, payload)
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
|
||||
}, [])
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getType = useCallback((variable?: ValueSelector) => {
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: variable || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return varType
|
||||
}, [getCurrentVariableType, availableNodes, isChatMode, iterationNode])
|
||||
|
||||
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variable_selector = variable as ValueSelector
|
||||
draft.is_array_file = getType(draft.variable_selector) === VarType.arrayFile
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getType, inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterVar,
|
||||
handleVarChanges,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ComparisonOperator, type Condition } from '../types'
|
||||
import {
|
||||
comparisonOperatorNotRequireValue,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
isEmptyRelatedOperator,
|
||||
} from '../utils'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
|
||||
import type { ValueSelector } from '../../../types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionValueProps = {
|
||||
condition: Condition
|
||||
}
|
||||
const ConditionValue = ({
|
||||
condition,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
variable_selector,
|
||||
comparison_operator: operator,
|
||||
sub_variable_condition,
|
||||
} = condition
|
||||
|
||||
const variableSelector = variable_selector as ValueSelector
|
||||
|
||||
const variableName = (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
const isEnvVar = isENV(variableSelector)
|
||||
const isChatVar = isConversationVar(variableSelector)
|
||||
const formatValue = useCallback((c: Condition) => {
|
||||
const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator)
|
||||
if (notHasValue)
|
||||
return ''
|
||||
|
||||
const value = c.value as string
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isSelect = useCallback((c: Condition) => {
|
||||
return c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
}, [])
|
||||
|
||||
const selectName = useCallback((c: Condition) => {
|
||||
const isSelect = c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
if (isSelect) {
|
||||
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0]
|
||||
return name
|
||||
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-workflow-block-parma-bg'>
|
||||
<div className='flex items-center px-1 h-6 '>
|
||||
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||
!notHasValue && 'max-w-[70px]',
|
||||
)}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 mx-1 text-xs font-medium text-text-primary'
|
||||
title={operatorName}
|
||||
>
|
||||
{operatorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-[10px] pl-[10px] border-l border-divider-regular'>
|
||||
{
|
||||
sub_variable_condition?.conditions.map((c: Condition, index) => (
|
||||
<div className='relative flex items-center h-6 space-x-1' key={c.id}>
|
||||
<div className='text-text-accent system-xs-medium'>{c.key}</div>
|
||||
<div className='text-text-primary system-xs-medium'>{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}</div>
|
||||
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) && <div className='text-text-secondary system-xs-regular'>{isSelect(c) ? selectName(c) : formatValue(c)}</div>}
|
||||
{index !== sub_variable_condition.conditions.length - 1 && (<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}</div>)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionValue)
|
||||
@ -0,0 +1,225 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDeleteBinLine,
|
||||
RiDraggable,
|
||||
} from '@remixicon/react'
|
||||
import type { CaseItem, HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, handleRemoveSubVariableCondition } from '../types'
|
||||
import type { Node, NodeOutPutVar, Var } from '../../../types'
|
||||
import { VarType } from '../../../types'
|
||||
import { useGetAvailableVars } from '../../variable-assigner/hooks'
|
||||
import { SUB_VARIABLES } from '../default'
|
||||
import ConditionList from './condition-list'
|
||||
import ConditionAdd from './condition-add'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect as Select } from '@/app/components/base/select'
|
||||
|
||||
type Props = {
|
||||
isSubVariable?: boolean
|
||||
caseId?: string
|
||||
conditionId?: string
|
||||
cases: CaseItem[]
|
||||
readOnly: boolean
|
||||
handleSortCase?: (sortedCases: (CaseItem & { id: string })[]) => void
|
||||
handleRemoveCase?: (caseId: string) => void
|
||||
handleAddCondition?: HandleAddCondition
|
||||
handleRemoveCondition?: HandleRemoveCondition
|
||||
handleUpdateCondition?: HandleUpdateCondition
|
||||
handleToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
|
||||
handleAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
handleRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
handleUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
handleToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
nodeId: string
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
varsIsVarFileAttribute?: Record<string, boolean>
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
}
|
||||
|
||||
const ConditionWrap: FC<Props> = ({
|
||||
isSubVariable,
|
||||
caseId,
|
||||
conditionId,
|
||||
nodeId: id = '',
|
||||
cases = [],
|
||||
readOnly,
|
||||
handleSortCase = () => { },
|
||||
handleRemoveCase,
|
||||
handleUpdateCondition,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
nodesOutputVars = [],
|
||||
availableNodes = [],
|
||||
varsIsVarFileAttribute = {},
|
||||
filterVar = () => true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
|
||||
const [willDeleteCaseId, setWillDeleteCaseId] = useState('')
|
||||
const casesLength = cases.length
|
||||
|
||||
const filterNumberVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.number
|
||||
}, [])
|
||||
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactSortable
|
||||
list={cases.map(caseItem => ({ ...caseItem, id: caseItem.case_id }))}
|
||||
setList={handleSortCase}
|
||||
handle='.handle'
|
||||
ghostClass='bg-components-panel-bg'
|
||||
animation={150}
|
||||
disabled={readOnly || isSubVariable}
|
||||
>
|
||||
{
|
||||
cases.map((item, index) => (
|
||||
<div key={item.case_id}>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative rounded-[10px] bg-components-panel-bg',
|
||||
willDeleteCaseId === item.case_id && 'bg-state-destructive-hover',
|
||||
!isSubVariable && 'py-1 px-3 min-h-[40px] ',
|
||||
isSubVariable && 'px-1 py-2',
|
||||
)}
|
||||
>
|
||||
{!isSubVariable && (
|
||||
<>
|
||||
<RiDraggable className={cn(
|
||||
'hidden handle absolute top-2 left-1 w-3 h-3 text-text-quaternary cursor-pointer',
|
||||
casesLength > 1 && 'group-hover:block',
|
||||
)} />
|
||||
<div className={cn(
|
||||
'absolute left-4 leading-4 text-[13px] font-semibold text-text-secondary',
|
||||
casesLength === 1 ? 'top-2.5' : 'top-1',
|
||||
)}>
|
||||
{
|
||||
index === 0 ? 'IF' : 'ELIF'
|
||||
}
|
||||
{
|
||||
casesLength > 1 && (
|
||||
<div className='text-[10px] text-text-tertiary font-medium'>CASE {index + 1}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
!!item.conditions.length && (
|
||||
<div className='mb-2'>
|
||||
<ConditionList
|
||||
disabled={readOnly}
|
||||
caseItem={item}
|
||||
caseId={isSubVariable ? caseId! : item.case_id}
|
||||
conditionId={conditionId}
|
||||
onUpdateCondition={handleUpdateCondition}
|
||||
onRemoveCondition={handleRemoveCondition}
|
||||
onToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
nodeId={id}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
filterVar={filterVar}
|
||||
numberVariables={getAvailableVars(id, '', filterNumberVar)}
|
||||
varsIsVarFileAttribute={varsIsVarFileAttribute}
|
||||
onAddSubVariableCondition={handleAddSubVariableCondition}
|
||||
onRemoveSubVariableCondition={handleRemoveSubVariableCondition}
|
||||
onUpdateSubVariableCondition={handleUpdateSubVariableCondition}
|
||||
onToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
|
||||
isSubVariable={isSubVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={cn(
|
||||
'flex items-center justify-between pr-[30px]',
|
||||
!item.conditions.length && !isSubVariable && 'mt-1',
|
||||
!item.conditions.length && isSubVariable && 'mt-2',
|
||||
!isSubVariable && ' pl-[60px]',
|
||||
)}>
|
||||
{isSubVariable
|
||||
? (
|
||||
<Select
|
||||
popupInnerClassName='w-[165px] max-h-none'
|
||||
onSelect={value => handleAddSubVariableCondition?.(caseId!, conditionId!, value.value as string)}
|
||||
items={subVarOptions}
|
||||
value=''
|
||||
renderTrigger={() => (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={readOnly}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ConditionAdd
|
||||
disabled={readOnly}
|
||||
caseId={item.case_id}
|
||||
variables={getAvailableVars(id, '', filterVar)}
|
||||
onSelectVariable={handleAddCondition!}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
((index === 0 && casesLength > 1) || (index > 0)) && (
|
||||
<Button
|
||||
className='hover:text-components-button-destructive-ghost-text hover:bg-components-button-destructive-ghost-bg-hover'
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={readOnly}
|
||||
onClick={() => handleRemoveCase?.(item.case_id)}
|
||||
onMouseEnter={() => setWillDeleteCaseId(item.case_id)}
|
||||
onMouseLeave={() => setWillDeleteCaseId('')}
|
||||
>
|
||||
<RiDeleteBinLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{!isSubVariable && (
|
||||
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</ReactSortable>
|
||||
{(cases.length === 0) && (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={readOnly}
|
||||
onClick={() => handleAddSubVariableCondition?.(caseId!, conditionId!)}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConditionWrap)
|
||||
@ -0,0 +1,45 @@
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useMemo } from 'react'
|
||||
import { useIsChatMode, useWorkflow, useWorkflowVariables } from '../../hooks'
|
||||
import type { ValueSelector } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
|
||||
type Params = {
|
||||
nodeId: string
|
||||
isInIteration: boolean
|
||||
}
|
||||
const useIsVarFileAttribute = ({
|
||||
nodeId,
|
||||
isInIteration,
|
||||
}: Params) => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === nodeId)
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(nodeId)
|
||||
}, [getBeforeNodesInSameBranch, nodeId])
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getIsVarFileAttribute = (variable: ValueSelector) => {
|
||||
if (variable.length !== 3)
|
||||
return false
|
||||
const parentVariable = variable.slice(0, 2)
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: parentVariable,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return varType === VarType.file
|
||||
}
|
||||
return {
|
||||
getIsVarFileAttribute,
|
||||
}
|
||||
}
|
||||
|
||||
export default useIsVarFileAttribute
|
||||
@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ConditionOperator from '../../if-else/components/condition-list/condition-operator'
|
||||
import { VarType } from '../../../types'
|
||||
import type { Condition } from '../types'
|
||||
import { ComparisonOperator } from '../../if-else/types'
|
||||
import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/utils'
|
||||
import SubVariablePicker from './sub-variable-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/if-else/default'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
|
||||
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
|
||||
type Props = {
|
||||
condition: Condition
|
||||
onChange: (condition: Condition) => void
|
||||
varType: VarType
|
||||
hasSubVariable: boolean
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const FilterCondition: FC<Props> = ({
|
||||
condition = { key: '', comparison_operator: ComparisonOperator.equal, value: '' },
|
||||
varType,
|
||||
onChange,
|
||||
hasSubVariable,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator)
|
||||
const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type'
|
||||
const selectOptions = useMemo(() => {
|
||||
if (isSelect) {
|
||||
if (condition.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) {
|
||||
return FILE_TYPE_OPTIONS.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
if (condition.key === 'transfer_method') {
|
||||
return TRANSFER_METHOD.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}, [condition.comparison_operator, condition.key, isSelect, t])
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: any) => {
|
||||
onChange({
|
||||
...condition,
|
||||
[key]: (isArrayValue && key === 'value') ? [value] : value,
|
||||
})
|
||||
}
|
||||
}, [condition, onChange, isArrayValue])
|
||||
|
||||
const handleSubVariableChange = useCallback((value: string) => {
|
||||
onChange({
|
||||
key: value,
|
||||
comparison_operator: getOperators(varType, { key: value })[0],
|
||||
value: '',
|
||||
})
|
||||
}, [onChange, varType])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasSubVariable && (
|
||||
<SubVariablePicker
|
||||
className="mb-2"
|
||||
value={condition.key}
|
||||
onChange={handleSubVariableChange}
|
||||
/>
|
||||
)}
|
||||
<div className='flex space-x-1'>
|
||||
<ConditionOperator
|
||||
className='h-8 bg-components-input-bg-normal'
|
||||
varType={varType}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleChange('comparison_operator')}
|
||||
file={hasSubVariable ? { key: condition.key } : undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && (
|
||||
<>
|
||||
{isSelect && (
|
||||
<Select
|
||||
items={selectOptions}
|
||||
defaultValue={isArrayValue ? (condition.value as string[])[0] : condition.value as string}
|
||||
onSelect={item => handleChange('value')(item.value)}
|
||||
className='!text-[13px]'
|
||||
wrapperClassName='grow h-8'
|
||||
placeholder='Select value'
|
||||
/>
|
||||
)}
|
||||
{!isSelect && (
|
||||
<Input
|
||||
type={((hasSubVariable && condition.key === 'size') || (!hasSubVariable && varType === VarType.number)) ? 'number' : 'text'}
|
||||
className='grow'
|
||||
value={condition.value}
|
||||
onChange={e => handleChange('value')(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FilterCondition)
|
||||
@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Limit } from '../types'
|
||||
import InputNumberWithSlider from '../../_base/components/input-number-with-slider'
|
||||
import cn from '@/utils/classnames'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
const LIMIT_SIZE_MIN = 1
|
||||
const LIMIT_SIZE_MAX = 20
|
||||
const LIMIT_SIZE_DEFAULT = 10
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
readonly: boolean
|
||||
config: Limit
|
||||
onChange: (limit: Limit) => void
|
||||
canSetRoleName?: boolean
|
||||
}
|
||||
|
||||
const LIMIT_DEFAULT: Limit = {
|
||||
enabled: false,
|
||||
size: LIMIT_SIZE_DEFAULT,
|
||||
}
|
||||
|
||||
const LimitConfig: FC<Props> = ({
|
||||
className,
|
||||
readonly,
|
||||
config = LIMIT_DEFAULT,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const payload = config
|
||||
|
||||
const handleLimitEnabledChange = useCallback((enabled: boolean) => {
|
||||
onChange({
|
||||
...config,
|
||||
enabled,
|
||||
})
|
||||
}, [config, onChange])
|
||||
|
||||
const handleLimitSizeChange = useCallback((size: number | string) => {
|
||||
onChange({
|
||||
...config,
|
||||
size: Number.parseInt(size as string),
|
||||
})
|
||||
}, [onChange, config])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.limit`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={payload.enabled}
|
||||
onChange={handleLimitEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{payload?.enabled
|
||||
? (
|
||||
<InputNumberWithSlider
|
||||
value={payload?.size || LIMIT_SIZE_DEFAULT}
|
||||
min={LIMIT_SIZE_MIN}
|
||||
max={LIMIT_SIZE_MAX}
|
||||
onChange={handleLimitSizeChange}
|
||||
readonly={readonly || !payload?.enabled}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LimitConfig)
|
||||
@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SUB_VARIABLES } from '../../if-else/default'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SubVariablePicker: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
value: item,
|
||||
name: item,
|
||||
}))
|
||||
|
||||
const renderOption = ({ item }: { item: Record<string, any> }) => {
|
||||
return (
|
||||
<div className='flex items-center h-6 justify-between'>
|
||||
<div className='flex items-center h-full'>
|
||||
<Variable02 className='mr-[5px] w-3.5 h-3.5 text-text-accent' />
|
||||
<span className='text-text-secondary system-sm-medium'>{item.name}</span>
|
||||
</div>
|
||||
<span className='text-text-tertiary system-xs-regular'>{item.type}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = useCallback(({ value }: Item) => {
|
||||
onChange(value as string)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Select
|
||||
items={subVarOptions}
|
||||
defaultValue={value}
|
||||
onSelect={handleChange}
|
||||
className='!text-[13px]'
|
||||
placeholder={t('workflow.nodes.listFilter.selectVariableKeyPlaceholder')!}
|
||||
optionClassName='pl-1 pr-5 py-0'
|
||||
renderOption={renderOption}
|
||||
renderTrigger={item => (
|
||||
<div className='group/sub-variable-picker flex items-center h-8 pl-1 rounded-lg bg-components-input-bg-normal hover:bg-state-base-hover-alt'>
|
||||
{item
|
||||
? <div className='flex justify-start cursor-pointer'>
|
||||
<div className='inline-flex max-w-full px-1.5 items-center h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark shadow-xs text-text-accent'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
|
||||
<div className='ml-0.5 truncate system-xs-medium'>{item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className='pl-1 flex text-components-input-text-placeholder system-sm-regular group-hover/sub-variable-picker:text-text-tertiary'>
|
||||
<Variable02 className='mr-1 shrink-0 w-4 h-4' />
|
||||
<span>{t('common.placeholder.select')}</span>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SubVariablePicker)
|
||||
61
web/app/components/workflow/nodes/list-operator/default.ts
Normal file
61
web/app/components/workflow/nodes/list-operator/default.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { comparisonOperatorNotRequireValue } from '../if-else/utils'
|
||||
import { type ListFilterNodeType, OrderBy } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
||||
defaultValue: {
|
||||
variable: [],
|
||||
filter_by: {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
},
|
||||
order_by: {
|
||||
enabled: false,
|
||||
key: '',
|
||||
value: OrderBy.ASC,
|
||||
},
|
||||
limit: {
|
||||
enabled: false,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: ListFilterNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable, var_type, filter_by } = payload
|
||||
|
||||
if (!errorMessages && !variable?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') })
|
||||
|
||||
// Check filter condition
|
||||
if (!errorMessages && filter_by?.enabled) {
|
||||
if (var_type === VarType.arrayFile && !filter_by.conditions[0]?.key)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionKey') })
|
||||
|
||||
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)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') })
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
42
web/app/components/workflow/nodes/list-operator/node.tsx
Normal file
42
web/app/components/workflow/nodes/list-operator/node.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
|
||||
import type { ListFilterNodeType } from './types'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
|
||||
const NodeComponent: FC<NodeProps<ListFilterNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
const { variable } = data
|
||||
|
||||
if (!variable || variable.length === 0)
|
||||
return null
|
||||
|
||||
const isSystem = isSystemVar(variable)
|
||||
const isEnv = isENV(variable)
|
||||
const isChatVar = isConversationVar(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
return (
|
||||
<div className='relative px-3'>
|
||||
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.inputVar`)}</div>
|
||||
<NodeVariableItem
|
||||
node={node as Node}
|
||||
isEnv={isEnv}
|
||||
isChatVar={isChatVar}
|
||||
varName={varName}
|
||||
className='bg-workflow-block-parma-bg'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NodeComponent)
|
||||
153
web/app/components/workflow/nodes/list-operator/panel.tsx
Normal file
153
web/app/components/workflow/nodes/list-operator/panel.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import OptionCard from '../_base/components/option-card'
|
||||
import Split from '../_base/components/split'
|
||||
import useConfig from './use-config'
|
||||
import SubVariablePicker from './components/sub-variable-picker'
|
||||
import { type ListFilterNodeType, OrderBy } from './types'
|
||||
import LimitConfig from './components/limit-config'
|
||||
import FilterCondition from './components/filter-condition'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
|
||||
const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
itemVarType,
|
||||
itemVarTypeShowName,
|
||||
hasSubVariable,
|
||||
handleVarChanges,
|
||||
filterVar,
|
||||
handleFilterEnabledChange,
|
||||
handleFilterChange,
|
||||
handleLimitChange,
|
||||
handleOrderByEnabledChange,
|
||||
handleOrderByKeyChange,
|
||||
handleOrderByTypeChange,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.variable || []}
|
||||
onChange={handleVarChanges}
|
||||
filterVar={filterVar}
|
||||
typePlaceHolder='Array'
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.filterCondition`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={inputs.filter_by?.enabled}
|
||||
onChange={handleFilterEnabledChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{inputs.filter_by?.enabled
|
||||
? (
|
||||
<FilterCondition
|
||||
condition={inputs.filter_by.conditions[0]}
|
||||
onChange={handleFilterChange}
|
||||
varType={itemVarType}
|
||||
hasSubVariable={hasSubVariable}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.orderBy`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={inputs.order_by?.enabled}
|
||||
onChange={handleOrderByEnabledChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{inputs.order_by?.enabled
|
||||
? (
|
||||
<div className='flex items-center justify-between'>
|
||||
{hasSubVariable && (
|
||||
<div className='grow mr-2'>
|
||||
<SubVariablePicker
|
||||
value={inputs.order_by.key as string}
|
||||
onChange={handleOrderByKeyChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={!hasSubVariable ? 'w-full grid grid-cols-2 gap-1' : 'shrink-0 flex space-x-1'}>
|
||||
<OptionCard
|
||||
title={t(`${i18nPrefix}.asc`)}
|
||||
onSelect={handleOrderByTypeChange(OrderBy.ASC)}
|
||||
selected={inputs.order_by.value === OrderBy.ASC}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t(`${i18nPrefix}.desc`)}
|
||||
onSelect={handleOrderByTypeChange(OrderBy.DESC)}
|
||||
selected={inputs.order_by.value === OrderBy.DESC}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
<Split />
|
||||
<LimitConfig
|
||||
config={inputs.limit}
|
||||
onChange={handleLimitChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='result'
|
||||
type={`Array[${itemVarTypeShowName}]`}
|
||||
description={t(`${i18nPrefix}.outputVars.result`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='first_record'
|
||||
type={itemVarTypeShowName}
|
||||
description={t(`${i18nPrefix}.outputVars.first_record`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='last_record'
|
||||
type={itemVarTypeShowName}
|
||||
description={t(`${i18nPrefix}.outputVars.last_record`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
34
web/app/components/workflow/nodes/list-operator/types.ts
Normal file
34
web/app/components/workflow/nodes/list-operator/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { ComparisonOperator } from '../if-else/types'
|
||||
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export enum OrderBy {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export type Limit = {
|
||||
enabled: boolean
|
||||
size?: number
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
key: string
|
||||
comparison_operator: ComparisonOperator
|
||||
value: string | number | string[]
|
||||
}
|
||||
|
||||
export type ListFilterNodeType = CommonNodeType & {
|
||||
variable: ValueSelector
|
||||
var_type: VarType // Cache for the type of output variable
|
||||
item_var_type: VarType // Cache for the type of output variable
|
||||
filter_by: {
|
||||
enabled: boolean
|
||||
conditions: Condition[]
|
||||
}
|
||||
order_by: {
|
||||
enabled: boolean
|
||||
key: ValueSelector | string
|
||||
value: OrderBy
|
||||
}
|
||||
limit: Limit
|
||||
}
|
||||
168
web/app/components/workflow/nodes/list-operator/use-config.ts
Normal file
168
web/app/components/workflow/nodes/list-operator/use-config.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import { getOperators } from '../if-else/utils'
|
||||
import { OrderBy } from './types'
|
||||
import type { Condition, Limit, ListFilterNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: ListFilterNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<ListFilterNodeType>(id, payload)
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getType = useCallback((variable?: ValueSelector) => {
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: variable || inputs.variable || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
let itemVarType = VarType.string
|
||||
switch (varType) {
|
||||
case VarType.arrayNumber:
|
||||
itemVarType = VarType.number
|
||||
break
|
||||
case VarType.arrayString:
|
||||
itemVarType = VarType.string
|
||||
break
|
||||
case VarType.arrayFile:
|
||||
itemVarType = VarType.file
|
||||
break
|
||||
case VarType.arrayObject:
|
||||
itemVarType = VarType.object
|
||||
break
|
||||
default:
|
||||
itemVarType = varType
|
||||
}
|
||||
return { varType, itemVarType }
|
||||
}, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, iterationNode])
|
||||
|
||||
const { varType, itemVarType } = getType()
|
||||
|
||||
const itemVarTypeShowName = useMemo(() => {
|
||||
if (!inputs.variable)
|
||||
return '?'
|
||||
return [(itemVarType || VarType.string).substring(0, 1).toUpperCase(), (itemVarType || VarType.string).substring(1)].join('')
|
||||
}, [inputs.variable, itemVarType])
|
||||
|
||||
const hasSubVariable = [VarType.arrayFile].includes(varType)
|
||||
|
||||
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variable = variable as ValueSelector
|
||||
const { varType, itemVarType } = getType(draft.variable)
|
||||
const isFileArray = varType === VarType.arrayFile
|
||||
|
||||
draft.var_type = varType
|
||||
draft.item_var_type = itemVarType
|
||||
draft.filter_by.conditions = [{
|
||||
key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '',
|
||||
comparison_operator: getOperators(itemVarType, isFileArray ? { key: 'name' } : undefined)[0],
|
||||
value: '',
|
||||
}]
|
||||
if (isFileArray && draft.order_by.enabled && !draft.order_by.key)
|
||||
draft.order_by.key = 'name'
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getType, inputs, setInputs])
|
||||
|
||||
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)
|
||||
}, [])
|
||||
|
||||
const handleFilterEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.filter_by.enabled = enabled
|
||||
if (enabled && !draft.filter_by.conditions)
|
||||
draft.filter_by.conditions = []
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [hasSubVariable, inputs, setInputs])
|
||||
|
||||
const handleFilterChange = useCallback((condition: Condition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.filter_by.conditions[0] = condition
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleLimitChange = useCallback((limit: Limit) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.limit = limit
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleOrderByEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.enabled = enabled
|
||||
if (enabled) {
|
||||
draft.order_by.value = OrderBy.ASC
|
||||
if (hasSubVariable && !draft.order_by.key)
|
||||
draft.order_by.key = 'name'
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [hasSubVariable, inputs, setInputs])
|
||||
|
||||
const handleOrderByKeyChange = useCallback((key: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.key = key
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleOrderByTypeChange = useCallback((type: OrderBy) => {
|
||||
return () => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.value = type
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterVar,
|
||||
varType,
|
||||
itemVarType,
|
||||
itemVarTypeShowName,
|
||||
hasSubVariable,
|
||||
handleVarChanges,
|
||||
handleFilterEnabledChange,
|
||||
handleFilterChange,
|
||||
handleLimitChange,
|
||||
handleOrderByEnabledChange,
|
||||
handleOrderByKeyChange,
|
||||
handleOrderByTypeChange,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@ -0,0 +1,56 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { GlobalVariable } from '../../types'
|
||||
import Item from './item'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Panel = () => {
|
||||
const { t } = useTranslation()
|
||||
const setShowPanel = useStore(s => s.setShowGlobalVariablePanel)
|
||||
|
||||
const globalVariableList: GlobalVariable[] = [
|
||||
{
|
||||
name: 'conversation_id',
|
||||
value_type: 'string',
|
||||
description: 'conversation id',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col w-[420px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
|
||||
)}
|
||||
>
|
||||
<div className='shrink-0 flex items-center justify-between p-4 pb-0 text-text-primary system-xl-semibold'>
|
||||
Global Variables(Current not show)
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowPanel(false)}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 py-1 px-4 system-sm-regular text-text-tertiary'>...</div>
|
||||
|
||||
<div className='grow px-4 rounded-b-2xl overflow-y-auto'>
|
||||
{globalVariableList.map(item => (
|
||||
<Item
|
||||
key={item.name}
|
||||
payload={item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Panel)
|
||||
@ -0,0 +1,30 @@
|
||||
import { memo } from 'react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import type { GlobalVariable } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
payload: GlobalVariable
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
payload,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'mb-1 px-2.5 py-2 bg-components-panel-on-panel-item-bg radius-md border border-components-panel-border-subtle shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
)}>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='grow flex gap-1 items-center'>
|
||||
<Env className='w-4 h-4 text-util-colors-violet-violet-600' />
|
||||
<div className='text-text-primary system-sm-medium'>{payload.name}</div>
|
||||
<div className='text-text-tertiary system-xs-medium'>{capitalize(payload.value_type)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-text-tertiary system-xs-regular truncate'>{payload.description}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Item)
|
||||
3
web/app/components/workflow/run/assets/bg-line-error.svg
Normal file
3
web/app/components/workflow/run/assets/bg-line-error.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#F04438" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#0BA5EC" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0H368M0 2H368M0 4H368M0 6H368M0 8H368M0 10H368M0 12H368M0 14H368M0 16H368M0 18H368M0 20H368M0 22H368M0 24H368M0 26H368M0 28H368M0 30H368M0 32H368M0 34H368M0 36H368M0 38H368M0 40H368M0 42H368M0 44H368M0 46H368M0 48H368M0 50H368M0 52H368M0 54H368M0 56H368M0 58H368M0 60H368M0 62H368M0 64H368M0 66H368M0 68H368M0 70H368M0 72H368M0 74H368M0 76H368M0 78H368M0 80H368M0 82H368M0 84H368M0 86H368M0 88H368M0 90H368M0 92H368M0 94H368M0 96H368M0 98H368M0 100H368M0 102H368M0 104H368M0 106H368M0 108H368M0 110H368M0 112H368M0 114H368M0 116H368M0 118H368M0 120H368M0 122H368M0 124H368M0 126H368M0 128H368M0 130H368M0 132H368M0 134H368M0 136H368M0 138H368M0 140H368M0 142H368M0 144H368M0 146H368M0 148H368M0 150H368M0 152H368M0 154H368M0 156H368M0 158H368M0 160H368M0 162H368M0 164H368M0 166H368M0 168H368M0 170H368M0 172H368M0 174H368M0 176H368M0 178H368M0 180H368M0 182H368M0 184H368M0 186H368M0 188H368M0 190H368M0 192H368M0 194H368M0 196H368M0 198H368M0 200H368M0 202H368M0 204H368M0 206H368M0 208H368M0 210H368M0 212H368M0 214H368M0 216H368M0 218H368M0 220H368M0 222H368M0 224H368M0 226H368" stroke="#17B26A" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="368" height="52" viewBox="0 0 368 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 0.5H368M0 2.5H368M0 4.5H368M0 6.5H368M0 8.5H368M0 10.5H368M0 12.5H368M0 14.5H368M0 16.5H368M0 18.5H368M0 20.5H368M0 22.5H368M0 24.5H368M0 26.5H368M0 28.5H368M0 30.5H368M0 32.5H368M0 34.5H368M0 36.5H368M0 38.5H368M0 40.5H368M0 42.5H368M0 44.5H368M0 46.5H368M0 48.5H368M0 50.5H368M0 52.5H368M0 54.5H368M0 56.5H368M0 58.5H368M0 60.5H368M0 62.5H368M0 64.5H368M0 66.5H368M0 68.5H368M0 70.5H368M0 72.5H368M0 74.5H368M0 76.5H368M0 78.5H368M0 80.5H368M0 82.5H368M0 84.5H368M0 86.5H368M0 88.5H368M0 90.5H368M0 92.5H368M0 94.5H368M0 96.5H368M0 98.5H368M0 100.5H368M0 102.5H368M0 104.5H368M0 106.5H368M0 108.5H368M0 110.5H368M0 112.5H368M0 114.5H368M0 116.5H368M0 118.5H368M0 120.5H368M0 122.5H368M0 124.5H368M0 126.5H368M0 128.5H368M0 130.5H368M0 132.5H368M0 134.5H368M0 136.5H368M0 138.5H368M0 140.5H368M0 142.5H368M0 144.5H368M0 146.5H368M0 148.5H368M0 150.5H368M0 152.5H368M0 154.5H368M0 156.5H368M0 158.5H368M0 160.5H368M0 162.5H368M0 164.5H368M0 166.5H368M0 168.5H368M0 170.5H368M0 172.5H368M0 174.5H368M0 176.5H368M0 178.5H368M0 180.5H368M0 182.5H368M0 184.5H368M0 186.5H368M0 188.5H368M0 190.5H368M0 192.5H368M0 194.5H368M0 196.5H368M0 198.5H368M0 200.5H368M0 202.5H368M0 204.5H368M0 206.5H368M0 208.5H368M0 210.5H368M0 212.5H368M0 214.5H368M0 216.5H368M0 218.5H368M0 220.5H368M0 222.5H368M0 224.5H368M0 226.5H368" stroke="#F79009" stroke-opacity="0.3" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
web/app/components/workflow/run/assets/highlight.svg
Normal file
9
web/app/components/workflow/run/assets/highlight.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="237" height="50" viewBox="0 0 237 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 8C0 3.58172 3.58172 0 8 0H237L215.033 50H8C3.58172 50 0 46.4183 0 42V8Z" fill="url(#paint0_linear_3552_29170)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3552_29170" x1="-4.89158e-08" y1="4.62963" x2="168.013" y2="23.1752" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 516 B |
30
web/app/components/workflow/run/status-container.tsx
Normal file
30
web/app/components/workflow/run/status-container.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
status: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const StatusContainer: FC<Props> = ({
|
||||
status,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative px-3 py-2.5 rounded-lg border system-xs-regular',
|
||||
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
|
||||
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-warning',
|
||||
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
|
||||
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-util-colors-blue-light-blue-light-600',
|
||||
)}
|
||||
>
|
||||
<div className='absolute top-0 left-0 w-[65%] h-[50px] bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]'></div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusContainer
|
||||
Reference in New Issue
Block a user