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:
Yeuoly
2025-11-12 17:59:37 +08:00
committed by GitHub
parent ca7794305b
commit b76e17b25d
785 changed files with 41186 additions and 3725 deletions

View File

@@ -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])

View File

@@ -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: {},

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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' />

View File

@@ -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

View File

@@ -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)

View File

@@ -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}
/>
)
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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[]

View File

@@ -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>

View File

@@ -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}
/>}

View File

@@ -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 && (
<>

View File

@@ -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
}

View File

@@ -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>
)

View File

@@ -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}

View File

@@ -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

View File

@@ -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>
}