feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import SearchInput from '@/app/components/base/search-input'
|
||||
import Tools from '../../../block-selector/tools'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { PluginType, type StrategyPluginDetail } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum, type StrategyPluginDetail } from '@/app/components/plugins/types'
|
||||
import type { ToolWithProvider } from '../../../types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
@@ -140,7 +140,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
||||
if (query) {
|
||||
fetchPlugins({
|
||||
query,
|
||||
category: PluginType.agent,
|
||||
category: PluginCategoryEnum.agent,
|
||||
})
|
||||
}
|
||||
}, [query])
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { Node } from 'reactflow'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type Strategy = {
|
||||
agent_strategy_provider_name: string
|
||||
@@ -99,7 +100,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
modelConfig={
|
||||
defaultModel.data
|
||||
? {
|
||||
mode: 'chat',
|
||||
mode: AppModeEnum.CHAT,
|
||||
name: defaultModel.data.model,
|
||||
provider: defaultModel.data.provider.provider,
|
||||
completion_params: {},
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { AppModeEnum } from '@/types/app'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
@@ -42,7 +42,7 @@ const CodeGenerateBtn: FC<Props> = ({
|
||||
</ActionButton>
|
||||
{showAutomatic && (
|
||||
<GetCodeGeneratorResModal
|
||||
mode={AppType.chat}
|
||||
mode={AppModeEnum.CHAT}
|
||||
isShow={showAutomatic}
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export enum StartNodeTypeEnum {
|
||||
Start = 'start',
|
||||
Trigger = 'trigger',
|
||||
}
|
||||
|
||||
type EntryNodeContainerProps = {
|
||||
children: ReactNode
|
||||
customLabel?: string
|
||||
nodeType?: StartNodeTypeEnum
|
||||
}
|
||||
|
||||
const EntryNodeContainer: FC<EntryNodeContainerProps> = ({
|
||||
children,
|
||||
customLabel,
|
||||
nodeType = StartNodeTypeEnum.Trigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const label = useMemo(() => {
|
||||
const translationKey = nodeType === StartNodeTypeEnum.Start ? 'entryNodeStatus' : 'triggerStatus'
|
||||
return customLabel || t(`workflow.${translationKey}.enabled`)
|
||||
}, [customLabel, nodeType, t])
|
||||
|
||||
return (
|
||||
<div className="w-fit min-w-[242px] rounded-2xl bg-workflow-block-wrapper-bg-1 px-0 pb-0 pt-0.5">
|
||||
<div className="mb-0.5 flex items-center px-1.5 pt-0.5">
|
||||
<span className="text-2xs font-semibold uppercase text-text-tertiary">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EntryNodeContainer
|
||||
@@ -1,38 +1,50 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { type ResourceVarInputs, VarKindType } from '../types'
|
||||
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import FormInputBoolean from './form-input-boolean'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
|
||||
import type { Event } from '@/app/components/tools/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import FormInputBoolean from './form-input-boolean'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ToolVarInputs
|
||||
value: ResourceVarInputs
|
||||
onChange: (value: any) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
currentTool?: Tool | Event
|
||||
currentProvider?: ToolWithProvider | TriggerWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
providerType?: string
|
||||
disableVariableInsertion?: boolean
|
||||
}
|
||||
|
||||
const FormInputItem: FC<Props> = ({
|
||||
@@ -46,15 +58,22 @@ const FormInputItem: FC<Props> = ({
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
extraParams,
|
||||
providerType,
|
||||
disableVariableInsertion = false,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
variable,
|
||||
type,
|
||||
_type,
|
||||
default: defaultValue,
|
||||
options,
|
||||
multiple,
|
||||
scope,
|
||||
} = schema as any
|
||||
const varInput = value[variable]
|
||||
@@ -64,13 +83,16 @@ const FormInputItem: FC<Props> = ({
|
||||
const isArray = type === FormTypeEnum.array
|
||||
const isShowJSONEditor = isObject || isArray
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isBoolean = type === FormTypeEnum.boolean
|
||||
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
|
||||
const isBoolean = _type === FormTypeEnum.boolean
|
||||
const isCheckbox = _type === FormTypeEnum.checkbox
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
|
||||
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
@@ -123,12 +145,71 @@ const FormInputItem: FC<Props> = ({
|
||||
const getVarKindType = () => {
|
||||
if (isFile)
|
||||
return VarKindType.variable
|
||||
if (isSelect || isBoolean || isNumber || isArray || isObject)
|
||||
if (isSelect || isDynamicSelect || isBoolean || isNumber || isArray || isObject)
|
||||
return VarKindType.constant
|
||||
if (isString)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
// Fetch dynamic options hook for tools
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '',
|
||||
currentProvider?.name || '',
|
||||
currentTool?.name || '',
|
||||
variable || '',
|
||||
providerType,
|
||||
extraParams,
|
||||
)
|
||||
|
||||
// Fetch dynamic options hook for triggers
|
||||
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
|
||||
plugin_id: currentProvider?.plugin_id || '',
|
||||
provider: currentProvider?.name || '',
|
||||
action: currentTool?.name || '',
|
||||
parameter: variable || '',
|
||||
extra: extraParams,
|
||||
credential_id: currentProvider?.credential_id || '',
|
||||
}, isDynamicSelect && providerType === PluginCategoryEnum.trigger && !!currentTool && !!currentProvider)
|
||||
|
||||
// Computed values for dynamic options (unified for triggers and tools)
|
||||
const triggerOptions = triggerDynamicOptions?.options
|
||||
const dynamicOptions = providerType === PluginCategoryEnum.trigger
|
||||
? triggerOptions ?? toolsOptions
|
||||
: toolsOptions
|
||||
const isLoadingOptions = providerType === PluginCategoryEnum.trigger
|
||||
? (isTriggerOptionsLoading || isLoadingToolsOptions)
|
||||
: isLoadingToolsOptions
|
||||
|
||||
// Fetch dynamic options for tools only (triggers use hook directly)
|
||||
useEffect(() => {
|
||||
const fetchPanelDynamicOptions = async () => {
|
||||
if (isDynamicSelect && currentTool && currentProvider && (providerType === PluginCategoryEnum.tool || providerType === PluginCategoryEnum.trigger)) {
|
||||
setIsLoadingToolsOptions(true)
|
||||
try {
|
||||
const data = await fetchDynamicOptions()
|
||||
setToolsOptions(data?.options || [])
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch dynamic options:', error)
|
||||
setToolsOptions([])
|
||||
}
|
||||
finally {
|
||||
setIsLoadingToolsOptions(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchPanelDynamicOptions()
|
||||
}, [
|
||||
isDynamicSelect,
|
||||
currentTool?.name,
|
||||
currentProvider?.name,
|
||||
variable,
|
||||
extraParams,
|
||||
providerType,
|
||||
fetchDynamicOptions,
|
||||
])
|
||||
|
||||
const handleTypeChange = (newType: string) => {
|
||||
if (newType === VarKindType.variable) {
|
||||
onChange({
|
||||
@@ -163,6 +244,24 @@ const FormInputItem: FC<Props> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectedLabels = (selectedValues: any[]) => {
|
||||
if (!selectedValues || selectedValues.length === 0)
|
||||
return ''
|
||||
|
||||
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
|
||||
const selectedOptions = optionsList.filter((opt: any) =>
|
||||
selectedValues.includes(opt.value),
|
||||
)
|
||||
|
||||
if (selectedOptions.length <= 2) {
|
||||
return selectedOptions
|
||||
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
return `${selectedOptions.length} selected`
|
||||
}
|
||||
|
||||
const handleAppOrModelSelect = (newValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
@@ -184,6 +283,45 @@ const FormInputItem: FC<Props> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const availableCheckboxOptions = useMemo(() => (
|
||||
(options || []).filter((option: { show_on?: Array<{ variable: string; value: any }> }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable]?.value === showOnItem.value || value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
})
|
||||
), [options, value])
|
||||
|
||||
const checkboxListOptions = useMemo(() => (
|
||||
availableCheckboxOptions.map((option: { value: string; label: Record<string, string> }) => ({
|
||||
value: option.value,
|
||||
label: option.label?.[language] || option.label?.en_US || option.value,
|
||||
}))
|
||||
), [availableCheckboxOptions, language])
|
||||
|
||||
const checkboxListValue = useMemo(() => {
|
||||
let current: string[] = []
|
||||
if (Array.isArray(varInput?.value))
|
||||
current = varInput.value as string[]
|
||||
else if (typeof varInput?.value === 'string')
|
||||
current = [varInput.value as string]
|
||||
else if (Array.isArray(defaultValue))
|
||||
current = defaultValue as string[]
|
||||
|
||||
const allowedValues = new Set(availableCheckboxOptions.map((option: { value: string }) => option.value))
|
||||
return current.filter(item => allowedValues.has(item))
|
||||
}, [varInput?.value, defaultValue, availableCheckboxOptions])
|
||||
|
||||
const handleCheckboxListChange = (selected: string[]) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.constant,
|
||||
value: selected,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
|
||||
{showTypeSwitch && (
|
||||
@@ -198,6 +336,7 @@ const FormInputItem: FC<Props> = ({
|
||||
availableNodes={availableNodesWithParent}
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
disableVariableInsertion={disableVariableInsertion}
|
||||
/>
|
||||
)}
|
||||
{isNumber && isConstant && (
|
||||
@@ -209,13 +348,23 @@ const FormInputItem: FC<Props> = ({
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isCheckbox && isConstant && (
|
||||
<CheckboxList
|
||||
title={schema.label?.[language] || schema.label?.en_US || variable}
|
||||
value={checkboxListValue}
|
||||
onChange={handleCheckboxListChange}
|
||||
options={checkboxListOptions}
|
||||
disabled={readOnly}
|
||||
maxHeight='200px'
|
||||
/>
|
||||
)}
|
||||
{isBoolean && isConstant && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
)}
|
||||
{isSelect && isConstant && (
|
||||
{isSelect && isConstant && !isMultipleSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly}
|
||||
@@ -225,11 +374,175 @@ const FormInputItem: FC<Props> = ({
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
renderOption={options.some((opt: any) => opt.icon) ? ({ item }) => (
|
||||
<div className="flex items-center">
|
||||
{item.icon && (
|
||||
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
)}
|
||||
{isSelect && isConstant && isMultipleSelect && (
|
||||
<Listbox
|
||||
multiple
|
||||
value={varInput?.value || []}
|
||||
onChange={handleValueChange}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<div className="group/simple-select relative h-8 grow">
|
||||
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
|
||||
<span className={cn('system-sm-regular block truncate text-left',
|
||||
varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
|
||||
)}>
|
||||
{getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ focus }) =>
|
||||
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
focus && 'bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
{option.icon && (
|
||||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
)}
|
||||
{isDynamicSelect && !isMultipleSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly || isLoadingOptions}
|
||||
defaultValue={varInput?.value}
|
||||
items={(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={isLoadingOptions ? 'Loading...' : (placeholder?.[language] || placeholder?.en_US)}
|
||||
renderOption={({ item }) => (
|
||||
<div className="flex items-center">
|
||||
{item.icon && (
|
||||
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDynamicSelect && isMultipleSelect && (
|
||||
<Listbox
|
||||
multiple
|
||||
value={varInput?.value || []}
|
||||
onChange={handleValueChange}
|
||||
disabled={readOnly || isLoadingOptions}
|
||||
>
|
||||
<div className="group/simple-select relative h-8 grow">
|
||||
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
|
||||
<span className={cn('system-sm-regular block truncate text-left',
|
||||
isLoadingOptions ? 'text-components-input-text-placeholder'
|
||||
: varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
|
||||
)}>
|
||||
{isLoadingOptions
|
||||
? 'Loading...'
|
||||
: getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoadingOptions ? (
|
||||
<RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ focus }) =>
|
||||
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
focus && 'bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
{option.icon && (
|
||||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
)}
|
||||
{isShowJSONEditor && isConstant && (
|
||||
<div className='mt-1 w-full'>
|
||||
<CodeEditor
|
||||
|
||||
@@ -1,37 +1,96 @@
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
|
||||
import type { ComponentProps, MouseEventHandler } from 'react'
|
||||
import { useState } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
|
||||
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
|
||||
uniqueIdentifier: string
|
||||
extraIdentifiers?: string[]
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
|
||||
const { className, uniqueIdentifier, onSuccess, ...rest } = props
|
||||
const {
|
||||
className,
|
||||
uniqueIdentifier,
|
||||
extraIdentifiers = [],
|
||||
onSuccess,
|
||||
...rest
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const identifiers = Array.from(new Set(
|
||||
[uniqueIdentifier, ...extraIdentifiers].filter((item): item is string => Boolean(item)),
|
||||
))
|
||||
const manifest = useCheckInstalled({
|
||||
pluginIds: [uniqueIdentifier],
|
||||
enabled: !!uniqueIdentifier,
|
||||
pluginIds: identifiers,
|
||||
enabled: identifiers.length > 0,
|
||||
})
|
||||
const install = useInstallPackageFromMarketPlace()
|
||||
const isLoading = manifest.isLoading || install.isPending
|
||||
// await for refetch to get the new installed plugin, when manifest refetch, this component will unmount
|
||||
|| install.isSuccess
|
||||
const [isTracking, setIsTracking] = useState(false)
|
||||
const isLoading = manifest.isLoading || install.isPending || isTracking
|
||||
const handleInstall: MouseEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
if (isLoading)
|
||||
return
|
||||
setIsTracking(true)
|
||||
install.mutate(uniqueIdentifier, {
|
||||
onSuccess: async () => {
|
||||
await manifest.refetch()
|
||||
onSuccess?.()
|
||||
onSuccess: async (response) => {
|
||||
const finish = async () => {
|
||||
await manifest.refetch()
|
||||
onSuccess?.()
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
await finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (response.all_installed) {
|
||||
await finish()
|
||||
return
|
||||
}
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
try {
|
||||
const { status } = await check({
|
||||
taskId: response.task_id,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
|
||||
if (status === TaskStatus.failed) {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
return
|
||||
}
|
||||
|
||||
await finish()
|
||||
}
|
||||
catch {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!manifest.data) return null
|
||||
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
|
||||
const identifierSet = new Set(identifiers)
|
||||
const isInstalled = manifest.data.plugins.some(plugin => (
|
||||
identifierSet.has(plugin.id)
|
||||
|| (plugin.plugin_unique_identifier && identifierSet.has(plugin.plugin_unique_identifier))
|
||||
|| (plugin.plugin_id && identifierSet.has(plugin.plugin_id))
|
||||
))
|
||||
if (isInstalled) return null
|
||||
return <Button
|
||||
variant={'secondary'}
|
||||
disabled={isLoading}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Placeholder from './placeholder'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type MixedVariableTextInputProps = {
|
||||
readOnly?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
value?: string
|
||||
onChange?: (text: string) => void
|
||||
}
|
||||
const MixedVariableTextInput = ({
|
||||
readOnly = false,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
value = '',
|
||||
onChange,
|
||||
}: MixedVariableTextInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PromptEditor
|
||||
wrapperClassName={cn(
|
||||
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
className='caret:text-text-accent'
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
placeholder={<Placeholder />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MixedVariableTextInput)
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { FOCUS_COMMAND } from 'lexical'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
const Placeholder = () => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const handleInsert = useCallback((text: string) => {
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode(text)
|
||||
$insertNodes([textNode])
|
||||
})
|
||||
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('')
|
||||
}}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onMouseDown={((e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
@@ -39,11 +39,11 @@ const Add = ({
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: nodeId,
|
||||
|
||||
@@ -38,8 +38,8 @@ const ChangeItem = ({
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesInteractions,
|
||||
} from '../../../hooks'
|
||||
import { type Node, NodeRunningStatus } from '../../../types'
|
||||
@@ -19,6 +18,9 @@ import {
|
||||
Stop,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useWorkflowRunValidation } from '@/app/components/workflow/hooks/use-checklist'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
type NodeControlProps = Pick<Node, 'id' | 'data'>
|
||||
const NodeControl: FC<NodeControlProps> = ({
|
||||
@@ -27,9 +29,11 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const { warningNodes } = useWorkflowRunValidation()
|
||||
const warningForNode = warningNodes.find(item => item.id === id)
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
@@ -38,7 +42,8 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute -top-7 right-0 hidden h-7 pb-1 group-hover:flex
|
||||
absolute -top-7 right-0 hidden h-7 pb-1
|
||||
${!data._pluginInstallLocked && 'group-hover:flex'}
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
@@ -50,17 +55,20 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
{
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<div
|
||||
className='flex h-5 w-5 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
className={`flex h-5 w-5 items-center justify-center rounded-md ${isSingleRunning ? 'cursor-pointer hover:bg-state-base-hover' : warningForNode ? 'cursor-not-allowed text-text-disabled' : 'cursor-pointer hover:bg-state-base-hover'}`}
|
||||
onClick={() => {
|
||||
const nextData: Record<string, any> = {
|
||||
_isSingleRun: !isSingleRunning,
|
||||
const action = isSingleRunning ? 'stop' : 'run'
|
||||
if (!isSingleRunning && warningForNode) {
|
||||
const message = warningForNode.errorMessage || t('workflow.panel.checklistTip')
|
||||
Toast.notify({ type: 'error', message })
|
||||
return
|
||||
}
|
||||
if(isSingleRunning)
|
||||
nextData._singleRunningStatus = undefined
|
||||
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: nextData,
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
nodeId: id,
|
||||
action,
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
}}
|
||||
@@ -70,7 +78,7 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
? <Stop className='h-3 w-3' />
|
||||
: (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
popupContent={warningForNode ? warningForNode.errorMessage || t('workflow.panel.checklistTip') : t('workflow.panel.runThisStep')}
|
||||
asChild={false}
|
||||
>
|
||||
<RiPlayLargeLine className='h-3 w-3' />
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../../../types'
|
||||
import type { Node } from '../../../types'
|
||||
import BlockSelector from '../../../block-selector'
|
||||
import type { DataSourceDefaultValue, ToolDefaultValue } from '../../../block-selector/types'
|
||||
import type { PluginDefaultValue } from '../../../block-selector/types'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../../../hooks'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../../../store'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
@@ -57,11 +58,11 @@ export const NodeTargetHandle = memo(({
|
||||
if (!connected)
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: id,
|
||||
@@ -84,7 +85,10 @@ export const NodeTargetHandle = memo(({
|
||||
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
|
||||
data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
|
||||
!connected && 'after:opacity-0',
|
||||
data.type === BlockEnum.Start && 'opacity-0',
|
||||
(data.type === BlockEnum.Start
|
||||
|| data.type === BlockEnum.TriggerWebhook
|
||||
|| data.type === BlockEnum.TriggerSchedule
|
||||
|| data.type === BlockEnum.TriggerPlugin) && 'opacity-0',
|
||||
handleClassName,
|
||||
)}
|
||||
isConnectable={isConnectable}
|
||||
@@ -124,7 +128,10 @@ export const NodeSourceHandle = memo(({
|
||||
showExceptionStatus,
|
||||
}: NodeHandleProps) => {
|
||||
const { t } = useTranslation()
|
||||
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
|
||||
const shouldAutoOpenStartNodeSelector = useStore(s => s.shouldAutoOpenStartNodeSelector)
|
||||
const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector)
|
||||
const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode)
|
||||
const workflowStoreApi = useWorkflowStore()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
@@ -140,11 +147,11 @@ export const NodeSourceHandle = memo(({
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: id,
|
||||
@@ -154,9 +161,27 @@ export const NodeSourceHandle = memo(({
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (notInitialWorkflow && data.type === BlockEnum.Start && !isChatMode)
|
||||
if (!shouldAutoOpenStartNodeSelector)
|
||||
return
|
||||
|
||||
if (isChatMode) {
|
||||
setShouldAutoOpenStartNodeSelector?.(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) {
|
||||
setOpen(true)
|
||||
}, [notInitialWorkflow, data.type, isChatMode])
|
||||
if (setShouldAutoOpenStartNodeSelector)
|
||||
setShouldAutoOpenStartNodeSelector(false)
|
||||
else
|
||||
workflowStoreApi?.setState?.({ shouldAutoOpenStartNodeSelector: false })
|
||||
|
||||
if (setHasSelectedStartNode)
|
||||
setHasSelectedStartNode(false)
|
||||
else
|
||||
workflowStoreApi?.setState?.({ hasSelectedStartNode: false })
|
||||
}
|
||||
}, [shouldAutoOpenStartNodeSelector, data.type, isChatMode, setShouldAutoOpenStartNodeSelector, setHasSelectedStartNode, workflowStoreApi])
|
||||
|
||||
return (
|
||||
<Handle
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { RiCrosshairLine } from '@remixicon/react'
|
||||
import { useReactFlow, useStore } from 'reactflow'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow-app/hooks'
|
||||
|
||||
type NodePositionProps = {
|
||||
nodeId: string
|
||||
}
|
||||
const NodePosition = ({
|
||||
nodeId,
|
||||
}: NodePositionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const reactflow = useReactFlow()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
nodePosition,
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
} = useStore(useShallow((s) => {
|
||||
const nodes = s.getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
return {
|
||||
nodePosition: currentNode.position,
|
||||
nodeWidth: currentNode.width,
|
||||
nodeHeight: currentNode.height,
|
||||
}
|
||||
}))
|
||||
const transform = useStore(s => s.transform)
|
||||
|
||||
if (!nodePosition || !nodeWidth || !nodeHeight) return null
|
||||
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
const zoom = transform[2]
|
||||
|
||||
const { clientWidth, clientHeight } = workflowContainer!
|
||||
const { setViewport } = reactflow
|
||||
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.panel.moveToThisNode')}
|
||||
>
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
setViewport({
|
||||
x: (clientWidth - 400 - nodeWidth * zoom) / 2 - nodePosition.x * zoom,
|
||||
y: (clientHeight - nodeHeight * zoom) / 2 - nodePosition.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
doSyncWorkflowDraft()
|
||||
}}
|
||||
>
|
||||
<RiCrosshairLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodePosition)
|
||||
@@ -8,12 +8,17 @@ import { intersection } from 'lodash-es'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import type {
|
||||
Node,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types'
|
||||
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
type ChangeBlockProps = {
|
||||
nodeId: string
|
||||
@@ -31,6 +36,14 @@ const ChangeBlock = ({
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
const isChatMode = useIsChatMode()
|
||||
const flowType = useHooksStore(s => s.configsMap?.flowType)
|
||||
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
|
||||
const ignoreNodeIds = useMemo(() => {
|
||||
if (isTriggerNode(nodeData.type as BlockEnum))
|
||||
return [nodeId]
|
||||
return undefined
|
||||
}, [nodeData.type, nodeId])
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availablePrevBlocks.length && availableNextBlocks.length)
|
||||
@@ -41,8 +54,8 @@ const ChangeBlock = ({
|
||||
return availableNextBlocks
|
||||
}, [availablePrevBlocks, availableNextBlocks])
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
|
||||
}, [handleNodeChange, nodeId, sourceHandle])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
@@ -64,6 +77,9 @@ const ChangeBlock = ({
|
||||
trigger={renderTrigger}
|
||||
popupClassName='min-w-[240px]'
|
||||
availableBlocksTypes={availableNodes}
|
||||
showStartTab={showStartTab}
|
||||
ignoreNodeIds={ignoreNodeIds}
|
||||
forceEnableStartTab={nodeData.type === BlockEnum.Start}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
VariableLabelInSelect,
|
||||
@@ -39,7 +39,8 @@ const VariableTag = ({
|
||||
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar
|
||||
const isGlobal = isGlobalVar(valueSelector)
|
||||
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
|
||||
|
||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||
const isException = isExceptionVariable(variableName, node?.data.type)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDropDownLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Field as FieldType } from '../../../../../llm/types'
|
||||
import { Type } from '../../../../../llm/types'
|
||||
import { getFieldType } from '../../../../../llm/utils'
|
||||
import type { Field as FieldType } from '../../../../../llm/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import TreeIndentLine from '../tree-indent-line'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { RiArrowDropDownLine } from '@remixicon/react'
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
@@ -28,6 +28,7 @@ const Field: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const isRoot = depth === 1
|
||||
const hasChildren = payload.type === Type.object && payload.properties
|
||||
const hasEnum = payload.enum && payload.enum.length > 0
|
||||
const [fold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(false)
|
||||
@@ -44,7 +45,10 @@ const Field: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>
|
||||
{getFieldType(payload)}
|
||||
{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}
|
||||
</div>
|
||||
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
|
||||
</div>
|
||||
{payload.description && (
|
||||
@@ -52,6 +56,18 @@ const Field: FC<Props> = ({
|
||||
<div className='system-xs-regular w-0 grow truncate text-text-tertiary'>{payload.description}</div>
|
||||
</div>
|
||||
)}
|
||||
{hasEnum && (
|
||||
<div className='ml-[7px] flex'>
|
||||
<div className='system-xs-regular w-0 grow text-text-quaternary'>
|
||||
{payload.enum!.map((value, index) => (
|
||||
<span key={index}>
|
||||
{typeof value === 'string' ? `"${value}"` : value}
|
||||
{index < payload.enum!.length - 1 && ' | '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ import type {
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { RAGPipelineVariable } from '@/models/pipeline'
|
||||
import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
|
||||
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
|
||||
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
|
||||
|
||||
import {
|
||||
AGENT_OUTPUT_STRUCT,
|
||||
@@ -51,6 +54,7 @@ import {
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
|
||||
TOOL_OUTPUT_STRUCT,
|
||||
getGlobalVars,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import ToolNodeDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
|
||||
@@ -59,11 +63,21 @@ import type { PromptItem } from '@/models/debug'
|
||||
import { VAR_REGEX } from '@/config'
|
||||
import type { AgentNodeType } from '../../../agent/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export const isSystemVar = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
|
||||
}
|
||||
|
||||
export const isGlobalVar = (valueSelector: ValueSelector) => {
|
||||
if(!isSystemVar(valueSelector)) return false
|
||||
const second = valueSelector[1]
|
||||
|
||||
if(['query', 'files'].includes(second))
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
export const isENV = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'env'
|
||||
}
|
||||
@@ -348,34 +362,29 @@ const formatItem = (
|
||||
variable: 'sys.query',
|
||||
type: VarType.string,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.dialogue_count',
|
||||
type: VarType.number,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.conversation_id',
|
||||
type: VarType.string,
|
||||
})
|
||||
}
|
||||
res.vars.push({
|
||||
variable: 'sys.user_id',
|
||||
type: VarType.string,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.files',
|
||||
type: VarType.arrayFile,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.app_id',
|
||||
type: VarType.string,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.workflow_id',
|
||||
type: VarType.string,
|
||||
})
|
||||
res.vars.push({
|
||||
variable: 'sys.workflow_run_id',
|
||||
type: VarType.string,
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.TriggerWebhook: {
|
||||
const {
|
||||
variables = [],
|
||||
} = data as WebhookTriggerNodeType
|
||||
res.vars = variables.map((v) => {
|
||||
const type = v.value_type || VarType.string
|
||||
const varRes: Var = {
|
||||
variable: v.variable,
|
||||
type,
|
||||
isParagraph: false,
|
||||
isSelect: false,
|
||||
options: v.options,
|
||||
required: v.required,
|
||||
}
|
||||
return varRes
|
||||
})
|
||||
|
||||
break
|
||||
@@ -612,6 +621,17 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.TriggerPlugin: {
|
||||
const outputSchema = PluginTriggerNodeDefault.getOutputVars?.(
|
||||
data as PluginTriggerNodeType,
|
||||
allPluginInfoList,
|
||||
[],
|
||||
{ schemaTypeDefinitions },
|
||||
) || []
|
||||
res.vars = outputSchema
|
||||
break
|
||||
}
|
||||
|
||||
case 'env': {
|
||||
res.vars = data.envList.map((env: EnvironmentVariable) => {
|
||||
return {
|
||||
@@ -634,6 +654,11 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case 'global': {
|
||||
res.vars = data.globalVarList
|
||||
break
|
||||
}
|
||||
|
||||
case 'rag': {
|
||||
res.vars = data.ragVariables.map((ragVar: RAGPipelineVariable) => {
|
||||
return {
|
||||
@@ -774,6 +799,15 @@ export const toNodeOutputVars = (
|
||||
chatVarList: conversationVariables,
|
||||
},
|
||||
}
|
||||
// GLOBAL_VAR_NODE data format
|
||||
const GLOBAL_VAR_NODE = {
|
||||
id: 'global',
|
||||
data: {
|
||||
title: 'SYSTEM',
|
||||
type: 'global',
|
||||
globalVarList: getGlobalVars(isChatMode),
|
||||
},
|
||||
}
|
||||
// RAG_PIPELINE_NODE data format
|
||||
const RAG_PIPELINE_NODE = {
|
||||
id: 'rag',
|
||||
@@ -793,6 +827,8 @@ export const toNodeOutputVars = (
|
||||
if (b.data.type === 'env') return -1
|
||||
if (a.data.type === 'conversation') return 1
|
||||
if (b.data.type === 'conversation') return -1
|
||||
if (a.data.type === 'global') return 1
|
||||
if (b.data.type === 'global') return -1
|
||||
// sort nodes by x position
|
||||
return (b.position?.x || 0) - (a.position?.x || 0)
|
||||
})
|
||||
@@ -803,6 +839,7 @@ export const toNodeOutputVars = (
|
||||
),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
...(isChatMode && conversationVariables.length > 0 ? [CHAT_VAR_NODE] : []),
|
||||
GLOBAL_VAR_NODE,
|
||||
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
|
||||
? [RAG_PIPELINE_NODE]
|
||||
: []),
|
||||
@@ -1026,7 +1063,8 @@ export const getVarType = ({
|
||||
if (valueSelector[1] === 'index') return VarType.number
|
||||
}
|
||||
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
const isGlobal = isGlobalVar(valueSelector)
|
||||
const isInStartNodeSysVar = isSystemVar(valueSelector) && !isGlobal
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const isSharedRagVariable
|
||||
@@ -1039,7 +1077,8 @@ export const getVarType = ({
|
||||
})
|
||||
|
||||
const targetVarNodeId = (() => {
|
||||
if (isSystem) return startNode?.id
|
||||
if (isInStartNodeSysVar) return startNode?.id
|
||||
if (isGlobal) return 'global'
|
||||
if (isInNodeRagVariable) return valueSelector[1]
|
||||
return valueSelector[0]
|
||||
})()
|
||||
@@ -1052,7 +1091,7 @@ export const getVarType = ({
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
|
||||
if (isSystem || isEnv || isChatVar || isSharedRagVariable) {
|
||||
if (isInStartNodeSysVar || isEnv || isChatVar || isSharedRagVariable || isGlobal) {
|
||||
return curr.find(
|
||||
(v: any) => v.variable === (valueSelector as ValueSelector).join('.'),
|
||||
)?.type
|
||||
@@ -1242,7 +1281,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
}
|
||||
case BlockEnum.LLM: {
|
||||
const payload = data as LLMNodeType
|
||||
const isChatModel = payload.model?.mode === 'chat'
|
||||
const isChatModel = payload.model?.mode === AppModeEnum.CHAT
|
||||
let prompts: string[] = []
|
||||
if (isChatModel) {
|
||||
prompts
|
||||
@@ -1545,7 +1584,7 @@ export const updateNodeVars = (
|
||||
}
|
||||
case BlockEnum.LLM: {
|
||||
const payload = data as LLMNodeType
|
||||
const isChatModel = payload.model?.mode === 'chat'
|
||||
const isChatModel = payload.model?.mode === AppModeEnum.CHAT
|
||||
if (isChatModel) {
|
||||
payload.prompt_template = (
|
||||
payload.prompt_template as PromptItem[]
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
import RemoveButton from '../remove-button'
|
||||
import useAvailableVarList from '../../hooks/use-available-var-list'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
|
||||
import ConstantField from './constant-field'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { CommonNodeType, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
// import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
@@ -45,9 +47,10 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import VarFullPathPanel from './var-full-path-panel'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
|
||||
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
@@ -78,7 +81,7 @@ type Props = {
|
||||
popupFor?: 'assigned' | 'toAssigned'
|
||||
zIndex?: number
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
currentProvider?: ToolWithProvider | TriggerWithProvider
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
|
||||
@@ -203,6 +206,9 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const varName = useMemo(() => {
|
||||
if (!hasValue)
|
||||
return ''
|
||||
const showName = VAR_SHOW_NAME_MAP[(value as ValueSelector).join('.')]
|
||||
if(showName)
|
||||
return showName
|
||||
|
||||
const isSystem = isSystemVar(value as ValueSelector)
|
||||
const varName = Array.isArray(value) ? value[(value as ValueSelector).length - 1] : ''
|
||||
@@ -291,15 +297,17 @@ const VarReferencePicker: FC<Props> = ({
|
||||
preferSchemaType,
|
||||
})
|
||||
|
||||
const { isEnv, isChatVar, isRagVar, isValidVar, isException } = useMemo(() => {
|
||||
const { isEnv, isChatVar, isGlobal, isRagVar, isValidVar, isException } = useMemo(() => {
|
||||
const isEnv = isENV(value as ValueSelector)
|
||||
const isChatVar = isConversationVar(value as ValueSelector)
|
||||
const isGlobal = isGlobalVar(value as ValueSelector)
|
||||
const isRagVar = isRagVariableVar(value as ValueSelector)
|
||||
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isRagVar
|
||||
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isGlobal || isRagVar
|
||||
const isException = isExceptionVariable(varName, outputVarNode?.type)
|
||||
return {
|
||||
isEnv,
|
||||
isChatVar,
|
||||
isGlobal,
|
||||
isRagVar,
|
||||
isValidVar,
|
||||
isException,
|
||||
@@ -392,10 +400,11 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const variableCategory = useMemo(() => {
|
||||
if (isEnv) return 'environment'
|
||||
if (isChatVar) return 'conversation'
|
||||
if (isGlobal) return 'global'
|
||||
if (isLoopVar) return 'loop'
|
||||
if (isRagVar) return 'rag'
|
||||
return 'system'
|
||||
}, [isEnv, isChatVar, isLoopVar, isRagVar])
|
||||
}, [isEnv, isChatVar, isGlobal, isLoopVar, isRagVar])
|
||||
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||
@@ -473,7 +482,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && !isRagVar && (
|
||||
{isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar && (
|
||||
<div className='flex items-center' onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation()
|
||||
@@ -501,10 +510,11 @@ const VarReferencePicker: FC<Props> = ({
|
||||
<div className='flex items-center text-text-accent'>
|
||||
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
|
||||
<VariableIconWithColor
|
||||
variables={value as ValueSelector}
|
||||
variableCategory={variableCategory}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
|
||||
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning', isGlobal && 'text-util-colors-orange-orange-600')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender
|
||||
import ManageInputField from './manage-input-field'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
@@ -82,10 +83,14 @@ const Item: FC<ItemProps> = ({
|
||||
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
|
||||
|
||||
const varName = useMemo(() => {
|
||||
if(VAR_SHOW_NAME_MAP[itemData.variable])
|
||||
return VAR_SHOW_NAME_MAP[itemData.variable]
|
||||
|
||||
if (!isFlat)
|
||||
return itemData.variable
|
||||
if (itemData.variable === 'current')
|
||||
return isInCodeGeneratorInstructionEditor ? 'current_code' : 'current_prompt'
|
||||
|
||||
return itemData.variable
|
||||
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
|
||||
|
||||
@@ -182,6 +187,7 @@ const Item: FC<ItemProps> = ({
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{!isFlat && <VariableIconWithColor
|
||||
variables={itemData.variable.split('.')}
|
||||
variableCategory={variableCategory}
|
||||
isExceptionVariable={isException}
|
||||
/>}
|
||||
|
||||
@@ -11,6 +11,7 @@ import VariableIcon from './variable-icon'
|
||||
import VariableName from './variable-name'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar } from '../../utils'
|
||||
|
||||
const VariableLabel = ({
|
||||
nodeType,
|
||||
@@ -26,6 +27,7 @@ const VariableLabel = ({
|
||||
rightSlot,
|
||||
}: VariablePayload) => {
|
||||
const varColorClassName = useVarColor(variables, isExceptionVariable)
|
||||
const isHideNodeLabel = !(isENV(variables) || isConversationVar(variables) || isGlobalVar(variables) || isRagVariableVar(variables))
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -35,10 +37,12 @@ const VariableLabel = ({
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<VariableNodeLabel
|
||||
nodeType={nodeType}
|
||||
nodeTitle={nodeTitle}
|
||||
/>
|
||||
{ isHideNodeLabel && (
|
||||
<VariableNodeLabel
|
||||
nodeType={nodeType}
|
||||
nodeTitle={nodeTitle}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
notShowFullPath && (
|
||||
<>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { BubbleX, Env, GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
isGlobalVar,
|
||||
isRagVariableVar,
|
||||
isSystemVar,
|
||||
} from '../utils'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useVarIcon = (variables: string[], variableCategory?: VarInInspectType | string) => {
|
||||
if (variableCategory === 'loop')
|
||||
@@ -24,6 +26,9 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
|
||||
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
||||
return BubbleX
|
||||
|
||||
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
|
||||
return GlobalVariable
|
||||
|
||||
return Variable02
|
||||
}
|
||||
|
||||
@@ -41,13 +46,22 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
|
||||
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
|
||||
return 'text-util-colors-teal-teal-700'
|
||||
|
||||
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
|
||||
return 'text-util-colors-orange-orange-600'
|
||||
|
||||
return 'text-text-accent'
|
||||
}, [variables, isExceptionVariable, variableCategory])
|
||||
}
|
||||
|
||||
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
|
||||
const showName = VAR_SHOW_NAME_MAP[variables.join('.')]
|
||||
let variableFullPathName = variables.slice(1).join('.')
|
||||
|
||||
if (isRagVariableVar(variables))
|
||||
variableFullPathName = variables.slice(2).join('.')
|
||||
|
||||
const varName = useMemo(() => {
|
||||
let variableFullPathName = variables.slice(1).join('.')
|
||||
variableFullPathName = variables.slice(1).join('.')
|
||||
|
||||
if (isRagVariableVar(variables))
|
||||
variableFullPathName = variables.slice(2).join('.')
|
||||
@@ -58,6 +72,8 @@ export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
|
||||
return `${isSystem ? 'sys.' : ''}${varName}`
|
||||
}, [variables, notShowFullPath])
|
||||
|
||||
if (showName)
|
||||
return showName
|
||||
return varName
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import React, {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NextStep from '../next-step'
|
||||
import PanelOperator from '../panel-operator'
|
||||
import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position'
|
||||
import HelpLink from '../help-link'
|
||||
import {
|
||||
DescriptionInput,
|
||||
TitleInput,
|
||||
} from '../title-description-input'
|
||||
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
|
||||
import RetryOnPanel from '../retry/retry-on-panel'
|
||||
import { useResizePanel } from '../../hooks/use-resize-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
AuthCategory,
|
||||
AuthorizedInDataSourceNode,
|
||||
AuthorizedInNode,
|
||||
PluginAuth,
|
||||
PluginAuthInDataSourceNode,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store'
|
||||
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import {
|
||||
WorkflowHistoryEvent,
|
||||
useAvailableBlocks,
|
||||
@@ -41,41 +23,59 @@ import {
|
||||
useToolIcon,
|
||||
useWorkflowHistory,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
|
||||
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useLogs } from '@/app/components/workflow/run/hooks'
|
||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import {
|
||||
canRunBySingle,
|
||||
hasErrorHandleNode,
|
||||
hasRetryNode,
|
||||
isSupportCustomRunForm,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import Tab, { TabType } from './tab'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { canFindTool } from '@/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { debounce } from 'lodash-es'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useResizePanel } from '../../hooks/use-resize-panel'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
import PanelWrap from '../before-run-form/panel-wrap'
|
||||
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
|
||||
import HelpLink from '../help-link'
|
||||
import NextStep from '../next-step'
|
||||
import PanelOperator from '../panel-operator'
|
||||
import RetryOnPanel from '../retry/retry-on-panel'
|
||||
import { DescriptionInput, TitleInput } from '../title-description-input'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useLogs } from '@/app/components/workflow/run/hooks'
|
||||
import PanelWrap from '../before-run-form/panel-wrap'
|
||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { FlowType } from '@/types/common'
|
||||
import {
|
||||
AuthorizedInDataSourceNode,
|
||||
AuthorizedInNode,
|
||||
PluginAuth,
|
||||
PluginAuthInDataSourceNode,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { canFindTool } from '@/utils'
|
||||
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import Tab, { TabType } from './tab'
|
||||
import { TriggerSubscription } from './trigger-subscription'
|
||||
|
||||
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
|
||||
const nodeType = params.payload.type
|
||||
@@ -86,6 +86,7 @@ const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
|
||||
return <div>Custom Run Form: {nodeType} not found</div>
|
||||
}
|
||||
}
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
id: Node['id']
|
||||
@@ -98,6 +99,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})))
|
||||
@@ -108,6 +110,13 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
||||
const otherPanelWidth = useStore(s => s.otherPanelWidth)
|
||||
const setNodePanelWidth = useStore(s => s.setNodePanelWidth)
|
||||
const {
|
||||
pendingSingleRun,
|
||||
setPendingSingleRun,
|
||||
} = useStore(s => ({
|
||||
pendingSingleRun: s.pendingSingleRun,
|
||||
setPendingSingleRun: s.setPendingSingleRun,
|
||||
}))
|
||||
|
||||
const reservedCanvasWidth = 400 // Reserve the minimum visible width for the canvas
|
||||
|
||||
@@ -212,6 +221,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
useEffect(() => {
|
||||
hasClickRunning.current = false
|
||||
}, [id])
|
||||
|
||||
const {
|
||||
nodesMap,
|
||||
} = useNodesMetaData()
|
||||
@@ -235,6 +245,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
singleRunParams,
|
||||
nodeInfo,
|
||||
setRunInputData,
|
||||
handleStop,
|
||||
handleSingleRun,
|
||||
handleRunWithParams,
|
||||
getExistVarValuesInForms,
|
||||
@@ -252,26 +263,65 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
setIsPaused(false)
|
||||
}, [tabType])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingSingleRun || pendingSingleRun.nodeId !== id)
|
||||
return
|
||||
|
||||
if (pendingSingleRun.action === 'run')
|
||||
handleSingleRun()
|
||||
else
|
||||
handleStop()
|
||||
|
||||
setPendingSingleRun(undefined)
|
||||
}, [pendingSingleRun, id, handleSingleRun, handleStop, setPendingSingleRun])
|
||||
|
||||
const logParams = useLogs()
|
||||
const passedLogParams = (() => {
|
||||
if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type))
|
||||
return logParams
|
||||
|
||||
return {}
|
||||
})()
|
||||
const passedLogParams = useMemo(() => [BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type) ? logParams : {}, [data.type, logParams])
|
||||
|
||||
const storeBuildInTools = useStore(s => s.buildInTools)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const currCollection = useMemo(() => {
|
||||
return buildInTools?.find(item => canFindTool(item.id, data.provider_id))
|
||||
}, [buildInTools, data.provider_id])
|
||||
const showPluginAuth = useMemo(() => {
|
||||
return data.type === BlockEnum.Tool && currCollection?.allow_delete
|
||||
}, [currCollection, data.type])
|
||||
const currToolCollection = useMemo(() => {
|
||||
const candidates = buildInTools ?? storeBuildInTools
|
||||
return candidates?.find(item => canFindTool(item.id, data.provider_id))
|
||||
}, [buildInTools, storeBuildInTools, data.provider_id])
|
||||
const needsToolAuth = useMemo(() => {
|
||||
return data.type === BlockEnum.Tool && currToolCollection?.allow_delete
|
||||
}, [data.type, currToolCollection?.allow_delete])
|
||||
|
||||
// only fetch trigger plugins when the node is a trigger plugin
|
||||
const { data: triggerPlugins = [] } = useAllTriggerPlugins(data.type === BlockEnum.TriggerPlugin)
|
||||
const currentTriggerPlugin = useMemo(() => {
|
||||
if (data.type !== BlockEnum.TriggerPlugin || !data.plugin_id || !triggerPlugins?.length)
|
||||
return undefined
|
||||
return triggerPlugins?.find(p => p.plugin_id === data.plugin_id)
|
||||
}, [data.type, data.plugin_id, triggerPlugins])
|
||||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTriggerPlugin?.subscription_constructor) {
|
||||
setDetail({
|
||||
name: currentTriggerPlugin.label[language],
|
||||
plugin_id: currentTriggerPlugin.plugin_id || '',
|
||||
plugin_unique_identifier: currentTriggerPlugin.plugin_unique_identifier || '',
|
||||
id: currentTriggerPlugin.id,
|
||||
provider: currentTriggerPlugin.name,
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_schema: currentTriggerPlugin.subscription_schema || [],
|
||||
subscription_constructor: currentTriggerPlugin.subscription_constructor,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [currentTriggerPlugin, language, setDetail])
|
||||
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
|
||||
const currentDataSource = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
|
||||
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
|
||||
}, [dataSourceList, data.plugin_id, data.type, data.provider_type])
|
||||
}, [dataSourceList, data.provider_id, data.type, data.provider_type])
|
||||
|
||||
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
@@ -280,15 +330,46 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
},
|
||||
})
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
||||
const handleJumpToDataSourcePage = useCallback(() => {
|
||||
setShowAccountSettingModal({ payload: 'data-source' })
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
}, [setShowAccountSettingModal])
|
||||
|
||||
const {
|
||||
appendNodeInspectVars,
|
||||
} = useInspectVarsCrud()
|
||||
|
||||
const handleSubscriptionChange = useCallback((v: SimpleSubscription, callback?: () => void) => {
|
||||
handleNodeDataUpdateWithSyncDraft(
|
||||
{ id, data: { subscription_id: v.id } },
|
||||
{
|
||||
sync: true,
|
||||
callback: { onSettled: callback },
|
||||
},
|
||||
)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
const readmeEntranceComponent = useMemo(() => {
|
||||
let pluginDetail
|
||||
switch (data.type) {
|
||||
case BlockEnum.Tool:
|
||||
pluginDetail = currToolCollection
|
||||
break
|
||||
case BlockEnum.DataSource:
|
||||
pluginDetail = currentDataSource
|
||||
break
|
||||
case BlockEnum.TriggerPlugin:
|
||||
pluginDetail = currentTriggerPlugin
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
return !pluginDetail ? null : <ReadmeEntrance pluginDetail={pluginDetail as any} className='mt-auto' />
|
||||
}, [data.type, currToolCollection, currentDataSource, currentTriggerPlugin])
|
||||
|
||||
if (logParams.showSpecialResultPanel) {
|
||||
return (
|
||||
<div className={cn(
|
||||
@@ -405,18 +486,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if (isSingleRunning) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (isSingleRunning)
|
||||
handleStop()
|
||||
else
|
||||
handleSingleRun()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{
|
||||
@@ -427,7 +500,6 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<NodePosition nodeId={id}></NodePosition>
|
||||
<HelpLink nodeType={data.type} />
|
||||
<PanelOperator id={id} data={data} showHelpLink={false} />
|
||||
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
|
||||
@@ -446,13 +518,14 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
showPluginAuth && (
|
||||
needsToolAuth && (
|
||||
<PluginAuth
|
||||
className='px-4 pb-2'
|
||||
pluginPayload={{
|
||||
provider: currCollection?.name || '',
|
||||
providerType: currCollection?.type || '',
|
||||
provider: currToolCollection?.name || '',
|
||||
providerType: currToolCollection?.type || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currToolCollection as any,
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
@@ -462,9 +535,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
/>
|
||||
<AuthorizedInNode
|
||||
pluginPayload={{
|
||||
provider: currCollection?.name || '',
|
||||
providerType: currCollection?.type || '',
|
||||
provider: currToolCollection?.name || '',
|
||||
providerType: currToolCollection?.type || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currToolCollection as any,
|
||||
}}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
credentialId={data.credential_id}
|
||||
@@ -493,7 +567,20 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!showPluginAuth && !currentDataSource && (
|
||||
currentTriggerPlugin && (
|
||||
<TriggerSubscription
|
||||
subscriptionIdSelected={data.subscription_id}
|
||||
onSubscriptionChange={handleSubscriptionChange}
|
||||
>
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
</TriggerSubscription>
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
@@ -505,7 +592,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
<Split />
|
||||
</div>
|
||||
{tabType === TabType.settings && (
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
<div className='flex flex-1 flex-col overflow-y-auto'>
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
@@ -550,6 +637,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -568,6 +656,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
{...passedLogParams}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -60,6 +60,19 @@ const LastRun: FC<Props> = ({
|
||||
const noLastRun = (error as any)?.status === 404
|
||||
const runResult = (canRunLastRun ? lastRunResult : singleRunResult) || lastRunResult || {}
|
||||
|
||||
const resolvedStatus = useMemo(() => {
|
||||
if (isPaused)
|
||||
return NodeRunningStatus.Stopped
|
||||
|
||||
if (oneStepRunRunningStatus === NodeRunningStatus.Stopped)
|
||||
return NodeRunningStatus.Stopped
|
||||
|
||||
if (oneStepRunRunningStatus === NodeRunningStatus.Listening)
|
||||
return NodeRunningStatus.Listening
|
||||
|
||||
return (runResult as any).status || otherResultPanelProps.status
|
||||
}, [isPaused, oneStepRunRunningStatus, runResult, otherResultPanelProps.status])
|
||||
|
||||
const resetHidePageStatus = useCallback(() => {
|
||||
setPageHasHide(false)
|
||||
setPageShowed(false)
|
||||
@@ -104,18 +117,18 @@ const LastRun: FC<Props> = ({
|
||||
|
||||
if (isRunning)
|
||||
return <ResultPanel status='running' showSteps={false} />
|
||||
|
||||
if (!isPaused && (noLastRun || !runResult)) {
|
||||
return (
|
||||
<NoData canSingleRun={canSingleRun} onSingleRun={onSingleRunClicked} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ResultPanel
|
||||
{...runResult as any}
|
||||
{...otherResultPanelProps}
|
||||
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
|
||||
status={resolvedStatus}
|
||||
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={runResult as NodeTracing}
|
||||
|
||||
@@ -22,6 +22,7 @@ import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/no
|
||||
import useKnowledgeBaseSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-base/use-single-run-form-params'
|
||||
|
||||
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
|
||||
import useTriggerPluginGetDataForCheckMore from '@/app/components/workflow/nodes/trigger-plugin/use-check-params'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
|
||||
// import
|
||||
@@ -30,10 +31,12 @@ import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useWorkflowRunValidation } from '@/app/components/workflow/hooks/use-checklist'
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: useLLMSingleRunFormParams,
|
||||
@@ -62,6 +65,9 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LoopEnd]: undefined,
|
||||
[BlockEnum.DataSource]: undefined,
|
||||
[BlockEnum.DataSourceEmpty]: undefined,
|
||||
[BlockEnum.TriggerWebhook]: undefined,
|
||||
[BlockEnum.TriggerSchedule]: undefined,
|
||||
[BlockEnum.TriggerPlugin]: undefined,
|
||||
}
|
||||
|
||||
const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => {
|
||||
@@ -97,6 +103,9 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.DataSource]: undefined,
|
||||
[BlockEnum.DataSourceEmpty]: undefined,
|
||||
[BlockEnum.KnowledgeBase]: undefined,
|
||||
[BlockEnum.TriggerWebhook]: undefined,
|
||||
[BlockEnum.TriggerSchedule]: undefined,
|
||||
[BlockEnum.TriggerPlugin]: useTriggerPluginGetDataForCheckMore,
|
||||
}
|
||||
|
||||
const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => {
|
||||
@@ -139,6 +148,17 @@ const useLastRun = <T>({
|
||||
isRunAfterSingleRun,
|
||||
})
|
||||
|
||||
const { warningNodes } = useWorkflowRunValidation()
|
||||
const blockIfChecklistFailed = useCallback(() => {
|
||||
const warningForNode = warningNodes.find(item => item.id === id)
|
||||
if (!warningForNode)
|
||||
return false
|
||||
|
||||
const message = warningForNode.errorMessage || 'This node has unresolved checklist issues'
|
||||
Toast.notify({ type: 'error', message })
|
||||
return true
|
||||
}, [warningNodes, id])
|
||||
|
||||
const {
|
||||
hideSingleRun,
|
||||
handleRun: doCallRunApi,
|
||||
@@ -199,7 +219,7 @@ const useLastRun = <T>({
|
||||
})
|
||||
}
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { setInitShowLastRunTab } = workflowStore.getState()
|
||||
const { setInitShowLastRunTab, setShowVariableInspectPanel } = workflowStore.getState()
|
||||
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
|
||||
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
|
||||
useEffect(() => {
|
||||
@@ -211,6 +231,8 @@ const useLastRun = <T>({
|
||||
const invalidLastRun = useInvalidLastRun(flowType, flowId, id)
|
||||
|
||||
const handleRunWithParams = async (data: Record<string, any>) => {
|
||||
if (blockIfChecklistFailed())
|
||||
return
|
||||
const { isValid } = checkValid()
|
||||
if (!isValid)
|
||||
return
|
||||
@@ -309,9 +331,13 @@ const useLastRun = <T>({
|
||||
}
|
||||
|
||||
const handleSingleRun = () => {
|
||||
if (blockIfChecklistFailed())
|
||||
return
|
||||
const { isValid } = checkValid()
|
||||
if (!isValid)
|
||||
return
|
||||
if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule)
|
||||
setShowVariableInspectPanel(true)
|
||||
if (isCustomRunNode) {
|
||||
showSingleRun()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create'
|
||||
import { SubscriptionSelectorEntry } from '@/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry'
|
||||
import { useSubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FC } from 'react'
|
||||
|
||||
type TriggerSubscriptionProps = {
|
||||
subscriptionIdSelected?: string
|
||||
onSubscriptionChange: (v: SimpleSubscription, callback?: () => void) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const TriggerSubscription: FC<TriggerSubscriptionProps> = ({ subscriptionIdSelected, onSubscriptionChange, children }) => {
|
||||
const { subscriptions } = useSubscriptionList()
|
||||
const subscriptionCount = subscriptions?.length || 0
|
||||
|
||||
return <div className={cn('px-4', subscriptionCount > 0 && 'flex items-center justify-between pr-3')}>
|
||||
{!subscriptionCount && <CreateSubscriptionButton buttonType={CreateButtonType.FULL_BUTTON} />}
|
||||
{children}
|
||||
{subscriptionCount > 0 && <SubscriptionSelectorEntry
|
||||
selectedId={subscriptionIdSelected}
|
||||
onSelect={onSubscriptionChange}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user