Feat/plugins (#12547)

Co-authored-by: AkaraChen <akarachen@outlook.com>
Co-authored-by: Yi <yxiaoisme@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: kurokobo <kuro664@gmail.com>
Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com>
This commit is contained in:
zxhlyh
2025-01-09 18:47:41 +08:00
committed by GitHub
parent e4c4490175
commit 3c014f3ae5
719 changed files with 48585 additions and 8553 deletions

View File

@ -7,7 +7,7 @@ import {
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
type Props = {
interface Props {
className?: string
text: string
onClick: () => void

View File

@ -0,0 +1,228 @@
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import type { ReactNode } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import type { Strategy } from './agent-strategy'
import classNames from '@/utils/classnames'
import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Link from 'next/link'
import { InstallPluginButton } from './install-plugin-button'
import ViewTypeSelect, { ViewType } from '../../../block-selector/view-type-select'
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 type { ToolWithProvider } from '../../../types'
import { CollectionType } from '@/app/components/tools/types'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useStrategyInfo } from '../../agent/use-config'
import { SwitchPluginVersion } from './switch-plugin-version'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
import { ToolTipContent } from '@/app/components/base/tooltip/content'
const NotFoundWarn = (props: {
title: ReactNode,
description: ReactNode
}) => {
const { title, description } = props
const { t } = useTranslation()
return <Tooltip
popupContent={
<div className='space-y-1 text-xs'>
<h3 className='text-text-primary font-semibold'>
{title}
</h3>
<p className='text-text-secondary tracking-tight'>
{description}
</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>
{t('workflow.nodes.agent.linkToPlugin')}
</Link>
</p>
</div>
}
needsDelay
>
<div>
<RiErrorWarningFill className='text-text-destructive size-4' />
</div>
</Tooltip>
}
function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => string): ToolWithProvider[] {
return input.map((item) => {
const res: ToolWithProvider = {
id: item.plugin_unique_identifier,
author: item.declaration.identity.author,
name: item.declaration.identity.name,
description: item.declaration.identity.description as any,
plugin_id: item.plugin_id,
icon: getIcon(item.declaration.identity.icon),
label: item.declaration.identity.label as any,
type: CollectionType.all,
tools: item.declaration.strategies.map(strategy => ({
name: strategy.identity.name,
author: strategy.identity.author,
label: strategy.identity.label as any,
description: strategy.description,
parameters: strategy.parameters as any,
output_schema: strategy.output_schema,
labels: [],
})),
team_credentials: {},
is_team_authorization: true,
allow_delete: false,
labels: [],
}
return res
})
}
export type AgentStrategySelectorProps = {
value?: Strategy,
onChange: (value?: Strategy) => void,
}
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
const { value, onChange } = props
const [open, setOpen] = useState(false)
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
const [query, setQuery] = useState('')
const stra = useStrategyProviders()
const { getIconUrl } = useGetIcon()
const list = stra.data ? formatStrategy(stra.data, getIconUrl) : undefined
const filteredTools = useMemo(() => {
if (!list) return []
return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase()))
}, [query, list])
const { strategyStatus, refetch: refetchStrategyInfo } = useStrategyInfo(
value?.agent_strategy_provider_name,
value?.agent_strategy_name,
)
const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external'
&& !strategyStatus.plugin.installed && !!value
const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external'
&& !strategyStatus?.isExistInPlugin && !!value
const showSwitchVersion = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed && !!value
const showInstallButton = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed && !!value
const icon = list?.find(
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
)?.icon as string | undefined
const { t } = useTranslation()
const wrapElemRef = useRef<HTMLDivElement>(null)
const {
queryPluginsWithDebounced: fetchPlugins,
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
useEffect(() => {
if (query) {
fetchPlugins({
query,
category: PluginType.agent,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
const pluginRef = useRef(null)
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
<PortalToFollowElemTrigger className='w-full'>
<div
className='h-8 p-1 gap-0.5 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt select-none'
onClick={() => setOpen(o => !o)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
{icon && <div className='flex items-center justify-center w-6 h-6'><img
src={icon}
width={20}
height={20}
className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
alt='icon'
/></div>}
<p
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')}
>
{value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}
</p>
<div className='ml-auto flex items-center gap-1'>
{showInstallButton && value && <InstallPluginButton
onClick={e => e.stopPropagation()}
size={'small'}
uniqueIdentifier={value.plugin_unique_identifier}
/>}
{showPluginNotInstalledWarn
? <NotFoundWarn
title={t('workflow.nodes.agent.pluginNotInstalled')}
description={t('workflow.nodes.agent.pluginNotInstalledDesc')}
/>
: showUnsupportedStrategy
? <NotFoundWarn
title={t('workflow.nodes.agent.unsupportedStrategy')}
description={t('workflow.nodes.agent.strategyNotFoundDesc')}
/>
: <RiArrowDownSLine className='size-4 text-text-tertiary' />
}
{showSwitchVersion && <SwitchPluginVersion
uniqueIdentifier={value.plugin_unique_identifier}
tooltip={<ToolTipContent
title={t('workflow.nodes.agent.unsupportedStrategy')}>
{t('workflow.nodes.agent.strategyNotFoundDescAndSwitchVersion')}
</ToolTipContent>}
onChange={() => {
refetchStrategyInfo()
}}
/>}
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='bg-components-panel-bg-blur border-components-panel-border border-[0.5px] rounded-md shadow overflow-hidden w-[388px]'>
<header className='p-2 gap-1 flex'>
<SearchInput placeholder={t('workflow.nodes.agent.strategy.searchPlaceholder')} value={query} onChange={setQuery} className={'w-full'} />
<ViewTypeSelect viewType={viewType} onChange={setViewType} />
</header>
<main className="md:max-h-[300px] xl:max-h-[400px] 2xl:max-h-[564px] relative overflow-hidden flex flex-col w-full" ref={wrapElemRef}>
<Tools
tools={filteredTools}
viewType={viewType}
onSelect={(_, tool) => {
onChange({
agent_strategy_name: tool!.tool_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
plugin_unique_identifier: tool!.provider_id,
})
setOpen(false)
} }
className='max-w-none max-h-full h-full overflow-y-auto'
indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} />
<PluginList
wrapElemRef={wrapElemRef}
list={notInstalledPlugins as any} ref={pluginRef}
searchText={query}
tags={[]}
disableMaxWidth
/>
</main>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
})
AgentStrategySelector.displayName = 'AgentStrategySelector'

View File

@ -0,0 +1,218 @@
import type { CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolVarInputs } from '../../tool/types'
import ListEmpty from '@/app/components/base/list-empty'
import { AgentStrategySelector } from './agent-strategy-selector'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { InputNumber } from '@/app/components/base/input-number'
import Slider from '@/app/components/base/slider'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import Field from './field'
import { type ComponentProps, memo } from 'react'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Editor from './prompt/editor'
import { useWorkflowStore } from '../../../store'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import type { NodeOutPutVar } from '../../../types'
import type { Node } from 'reactflow'
export type Strategy = {
agent_strategy_provider_name: string
agent_strategy_name: string
agent_strategy_label: string
agent_output_schema: Record<string, any>
plugin_unique_identifier: string
}
export type AgentStrategyProps = {
strategy?: Strategy
onStrategyChange: (strategy?: Strategy) => void
formSchema: CredentialFormSchema[]
formValue: ToolVarInputs
onFormValueChange: (value: ToolVarInputs) => void
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
}
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
type ToolSelectorSchema = CustomSchema<'tool-selector'>
type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes } = props
const { t } = useTranslation()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const renderI18nObject = useRenderI18nObject()
const workflowStore = useWorkflowStore()
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()
const override: ComponentProps<typeof Form<CustomField>>['override'] = [
[FormTypeEnum.textNumber, FormTypeEnum.textInput],
(schema, props) => {
switch (schema.type) {
case FormTypeEnum.textInput: {
const def = schema as CredentialFormSchemaTextInput
const value = props.value[schema.variable]
const onChange = (value: string) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
const handleGenerated = (value: string) => {
onChange(value)
setControlPromptEditorRerenderKey(Math.random())
}
return <Editor
value={value}
onChange={onChange}
onGenerated={handleGenerated}
title={renderI18nObject(schema.label)}
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerBackgroundClassName='bg-transparent'
gradientBorder={false}
isSupportPromptGenerator={!!def.auto_generate?.type}
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
editorContainerClassName='px-0'
availableNodes={availableNodes}
nodesOutputVars={nodeOutputVars}
isSupportJinja={def.template?.enabled}
required={def.required}
varList={[]}
modelConfig={
defaultModel.data
? {
mode: 'chat',
name: defaultModel.data.model,
provider: defaultModel.data.provider.provider,
completion_params: {},
} : undefined
}
placeholderClassName='px-2 py-1'
titleClassName='system-sm-semibold-uppercase text-text-secondary text-[13px]'
inputClassName='px-2 py-1 bg-components-input-bg-normal focus:bg-components-input-bg-active focus:border-components-input-border-active focus:border rounded-lg'
/>
}
case FormTypeEnum.textNumber: {
const def = schema as CredentialFormSchemaNumberInput
if (!def.max || !def.min)
return false
const defaultValue = schema.default ? Number.parseInt(schema.default) : 1
const value = props.value[schema.variable] || defaultValue
const onChange = (value: number) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return <Field
title={<>
{renderI18nObject(def.label)} {def.required && <span className='text-red-500'>*</span>}
</>}
tooltip={def.tooltip && renderI18nObject(def.tooltip)}
inline
>
<div className='flex w-[200px] items-center gap-3'>
<Slider
value={value}
onChange={onChange}
className='w-full'
min={def.min}
max={def.max}
/>
<InputNumber
value={value}
// TODO: maybe empty, handle this
onChange={onChange as any}
defaultValue={defaultValue}
size='sm'
min={def.min}
max={def.max}
className='w-12'
/>
</div>
</Field>
}
}
},
]
const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => {
switch (schema.type) {
case 'tool-selector': {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return (
<Field
title={<>
{renderI18nObject(schema.label)} {schema.required && <span className='text-red-500'>*</span>}
</>}
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
>
<ToolSelector
scope={schema.scope}
value={value}
onSelect={item => onChange(item)}
onDelete={() => onChange(null)}
/>
</Field>
)
}
case 'array[tools]': {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return (
<MultipleToolSelector
scope={schema.scope}
value={value || []}
label={renderI18nObject(schema.label)}
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
onChange={onChange}
supportCollapse
required={schema.required}
/>
)
}
}
}
return <div className='space-y-2'>
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
{
strategy
? <div>
<Form<CustomField>
formSchemas={[
...formSchema,
]}
value={formValue}
onChange={onFormValueChange}
validating={false}
showOnVariableMap={{}}
isEditMode={true}
isAgentStrategy={true}
fieldLabelClassName='uppercase'
customRenderField={renderField}
override={override}
/>
</div>
: <ListEmpty
icon={<Agent className='w-5 h-5 shrink-0 text-text-accent' />}
title={t('workflow.nodes.agent.strategy.configureTip')}
description={<div className='text-text-tertiary text-xs'>
{t('workflow.nodes.agent.strategy.configureTipDesc')} <br />
<Link href={'/'} className='text-text-accent-secondary'>
{t('workflow.nodes.agent.learnMore')}
</Link>
</div>}
/>
}
</div>
})
AgentStrategy.displayName = 'AgentStrategy'

View File

@ -25,7 +25,7 @@ import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import cn from '@/utils/classnames'
type Props = {
interface Props {
payload: InputVar
value: any
onChange: (value: any) => void

View File

@ -9,7 +9,7 @@ import { InputVarType } from '@/app/components/workflow/types'
import AddButton from '@/app/components/base/button/add-button'
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
export type Props = {
export interface Props {
className?: string
label?: string
inputs: InputVar[]

View File

@ -17,10 +17,10 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import type { NodeTracing } from '@/types/workflow'
import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { Emoji } from '@/app/components/tools/types'
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
const i18nPrefix = 'workflow.singleRun'
@ -34,13 +34,12 @@ type BeforeRunFormProps = {
runningStatus: NodeRunningStatus
result?: JSX.Element
forms: FormProps[]
retryDetails?: NodeTracing[]
onRetryDetailBack?: any
}
showSpecialResultPanel?: boolean
} & Partial<SpecialResultPanelProps>
function formatValue(value: string | any, type: InputVarType) {
if (type === InputVarType.number)
return parseFloat(value)
return Number.parseFloat(value)
if (type === InputVarType.json)
return JSON.parse(value)
if (type === InputVarType.contexts) {
@ -66,8 +65,8 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
runningStatus,
result,
forms,
retryDetails,
onRetryDetailBack = () => { },
showSpecialResultPanel,
...restResultPanelParams
}) => {
const { t } = useTranslation()
@ -141,24 +140,14 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
</div>
</div>
{
retryDetails?.length && (
showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<RetryResultPanel
list={retryDetails.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
node_type: nodeType!,
extras: {
icon: toolIcon!,
},
}))}
onBack={onRetryDetailBack}
/>
<SpecialResultPanel {...restResultPanelParams} />
</div>
)
}
{
!retryDetails?.length && (
!showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (

View File

@ -1,8 +1,9 @@
import type { ReactNode } from 'react'
import Collapse from '.'
type FieldCollapseProps = {
title: string
children: JSX.Element
children: ReactNode
}
const FieldCollapse = ({
title,

View File

@ -16,7 +16,7 @@ import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-tog
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
type Props = {
interface Props {
className?: string
title: JSX.Element | string
headerRight?: JSX.Element

View File

@ -88,7 +88,7 @@ const CodeEditor: FC<Props> = ({
const index = (() => {
if (match)
return parseInt(match[1]!) + 1
return Number.parseInt(match[1]!) + 1
return 1
})()

View File

@ -1,18 +1,17 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
import type { DefaultTFuncReturn } from 'i18next'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
title: JSX.Element | string | DefaultTFuncReturn
tooltip?: React.ReactNode
title: ReactNode
tooltip?: ReactNode
isSubTitle?: boolean
supportFold?: boolean
children?: JSX.Element | string | null
@ -51,7 +50,7 @@ const Field: FC<Props> = ({
<div className='flex'>
{operations && <div>{operations}</div>}
{supportFold && (
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary cursor-pointer transform transition-transform' style={{ transform: fold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary cursor-pointer transition-transform' style={{ transform: fold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
)}
</div>
</div>

View File

@ -0,0 +1,25 @@
import classNames from '@/utils/classnames'
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'
export type GroupLabelProps = ComponentProps<'div'>
export const GroupLabel: FC<GroupLabelProps> = (props) => {
const { children, className, ...rest } = props
return <div {...rest} className={classNames('mb-1 system-2xs-medium-uppercase text-text-tertiary', className)}>
{children}
</div>
}
export type Group = PropsWithChildren<{
label: ReactNode
}>
export const Group: FC<Group> = (props) => {
const { children, label } = props
return <div className={classNames('py-1')}>
{label}
<div className='space-y-0.5'>
{children}
</div>
</div>
}

View File

@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
type Props = {
title: string
content: string | JSX.Element
content: ReactNode
}
const InfoPanel: FC<Props> = ({

View File

@ -34,7 +34,7 @@ const InputNumberWithSlider: FC<Props> = ({
}, [defaultValue, max, min, onChange, value])
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(parseFloat(e.target.value))
onChange(Number.parseFloat(e.target.value))
}, [onChange])
return (

View File

@ -4,7 +4,7 @@ import React from 'react'
import { RiAlignLeft, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import { InputVarType } from '../../../types'
type Props = {
interface Props {
className?: string
type: InputVarType
}

View File

@ -0,0 +1,45 @@
import Button from '@/app/components/base/button'
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
import type { ComponentProps, MouseEventHandler } from 'react'
import classNames from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
uniqueIdentifier: string
onSuccess?: () => void
}
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
const { className, uniqueIdentifier, onSuccess, ...rest } = props
const { t } = useTranslation()
const manifest = useCheckInstalled({
pluginIds: [uniqueIdentifier],
enabled: !!uniqueIdentifier,
})
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 handleInstall: MouseEventHandler = (e) => {
e.stopPropagation()
install.mutate(uniqueIdentifier, {
onSuccess: async () => {
await manifest.refetch()
onSuccess?.()
},
})
}
if (!manifest.data) return null
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
return <Button
variant={'secondary'}
disabled={isLoading}
{...rest}
onClick={handleInstall}
className={classNames('flex items-center', className)}
>
{!isLoading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
{!isLoading ? <RiInstallLine className='size-3.5 ml-1' /> : <RiLoader2Line className='size-3.5 ml-1 animate-spin' />}
</Button>
}

View File

@ -88,7 +88,7 @@ const MemoryConfig: FC<Props> = ({
limitedSize = null
}
else {
limitedSize = parseInt(limitedSize as string, 10)
limitedSize = Number.parseInt(limitedSize as string, 10)
if (isNaN(limitedSize))
limitedSize = WINDOW_SIZE_DEFAULT

View File

@ -0,0 +1,43 @@
import {
RiAlertFill,
RiCheckboxCircleFill,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import cn from '@/utils/classnames'
type NodeStatusIconProps = {
status: string
className?: string
}
const NodeStatusIcon = ({
status,
className,
}: NodeStatusIconProps) => {
return (
<>
{
status === 'succeeded' && (
<RiCheckboxCircleFill className={cn('shrink-0 w-4 h-4 text-text-success', className)} />
)
}
{
status === 'failed' && (
<RiErrorWarningLine className={cn('shrink-0 w-4 h-4 text-text-warning', className)} />
)
}
{
(status === 'stopped' || status === 'exception') && (
<RiAlertFill className={cn('shrink-0 w-4 h-4 text-text-warning-secondary', className)} />
)
}
{
status === 'running' && (
<RiLoader2Line className={cn('shrink-0 w-4 h-4 text-text-accent animate-spin', className)} />
)
}
</>
)
}
export default NodeStatusIcon

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
@ -7,7 +7,7 @@ import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/
type Props = {
className?: string
title?: string
children: JSX.Element
children: ReactNode
}
const OutputVars: FC<Props> = ({

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React, { useCallback, useRef } from 'react'
import {
RiDeleteBinLine,
@ -68,6 +68,14 @@ type Props = {
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
containerBackgroundClassName?: string
gradientBorder?: boolean
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholderClassName?: string
titleClassName?: string
required?: boolean
}
const Editor: FC<Props> = ({
@ -96,6 +104,14 @@ const Editor: FC<Props> = ({
handleAddVariable,
onGenerated,
modelConfig,
containerBackgroundClassName: containerClassName,
gradientBorder = true,
titleTooltip,
inputClassName,
placeholderClassName,
titleClassName,
editorContainerClassName,
required,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@ -129,10 +145,13 @@ const Editor: FC<Props> = ({
return (
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
<div className={cn(headerClassName, 'pt-1 pl-3 pr-2 flex justify-between items-center')}>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
<div ref={ref} className={cn(isFocus ? (gradientBorder && s.gradientBorder) : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('pt-1 pl-3 pr-2 flex justify-between items-center', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('leading-4 text-xs font-semibold text-gray-700 uppercase', titleClassName)}>{title} {required && <span className='text-red-500'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
@ -201,12 +220,13 @@ const Editor: FC<Props> = ({
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className='min-h-[56px]'
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
@ -254,7 +274,7 @@ const Editor: FC<Props> = ({
</div>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
@ -266,6 +286,7 @@ const Editor: FC<Props> = ({
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
)}

View File

@ -0,0 +1,28 @@
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { type ComponentProps, type PropsWithChildren, type ReactNode, memo } from 'react'
export type SettingItemProps = PropsWithChildren<{
label: string
status?: 'error' | 'warning'
tooltip?: ReactNode
}>
export const SettingItem = memo(({ label, children, status, tooltip }: SettingItemProps) => {
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const needTooltip = ['error', 'warning'].includes(status as any)
return <div className='flex items-center justify-between bg-workflow-block-parma-bg rounded-md py-1 px-1.5 space-x-1 text-xs font-normal relative'>
<div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}>
{label}
</div>
<Tooltip popupContent={tooltip} disabled={!needTooltip}>
<div className='truncate text-right system-xs-medium text-text-secondary'>
{children}
</div>
</Tooltip>
{indicator && <Indicator color={indicator} className='absolute -right-0.5 -top-0.5' />}
</div>
})
SettingItem.displayName = 'SettingItem'

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
interface Props {
className?: string
}

View File

@ -0,0 +1,126 @@
'use client'
import Badge from '@/app/components/base/badge'
import Tooltip from '@/app/components/base/tooltip'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
import { RiArrowLeftRightLine, RiExternalLinkLine } from '@remixicon/react'
import type { ReactNode } from 'react'
import { type FC, useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useCheckInstalled, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
import cn from '@/utils/classnames'
import PluginMutationModel from '@/app/components/plugins/plugin-mutation-model'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { marketplaceUrlPrefix } from '@/config'
export type SwitchPluginVersionProps = {
uniqueIdentifier: string
tooltip?: ReactNode
onChange?: (version: string) => void
className?: string
}
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
const { uniqueIdentifier, tooltip, onChange, className } = props
const [pluginId] = uniqueIdentifier.split(':')
const [isShow, setIsShow] = useState(false)
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
const [target, setTarget] = useState<{
version: string,
pluginUniqueIden: string;
}>()
const pluginDetails = useCheckInstalled({
pluginIds: [pluginId],
enabled: true,
})
const pluginDetail = pluginDetails.data?.plugins.at(0)
const handleUpdatedFromMarketplace = useCallback(() => {
hideUpdateModal()
pluginDetails.refetch()
onChange?.(target!.version)
}, [hideUpdateModal, onChange, pluginDetails, target])
const { getIconUrl } = useGetIcon()
const icon = pluginDetail?.declaration.icon ? getIconUrl(pluginDetail.declaration.icon) : undefined
const mutation = useUpdatePackageFromMarketPlace()
const install = () => {
mutation.mutate(
{
new_plugin_unique_identifier: target!.pluginUniqueIden,
original_plugin_unique_identifier: uniqueIdentifier,
},
{
onSuccess() {
handleUpdatedFromMarketplace()
},
})
}
const { t } = useTranslation()
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
<div className={cn('w-fit flex items-center justify-center', className)} onClick={e => e.stopPropagation()}>
{isShowUpdateModal && pluginDetail && <PluginMutationModel
onCancel={hideUpdateModal}
plugin={pluginManifestToCardPluginProps({
...pluginDetail.declaration,
icon: icon!,
})}
mutation={mutation}
mutate={install}
confirmButtonText={t('workflow.nodes.agent.installPlugin.install')}
cancelButtonText={t('workflow.nodes.agent.installPlugin.cancel')}
modelTitle={t('workflow.nodes.agent.installPlugin.title')}
description={t('workflow.nodes.agent.installPlugin.desc')}
cardTitleLeft={<>
<Badge2 className='mx-1' size="s" state={BadgeState.Warning}>
{`${pluginDetail.version} -> ${target!.version}`}
</Badge2>
</>}
modalBottomLeft={
<Link
className='flex justify-center items-center gap-1'
href={`${marketplaceUrlPrefix}/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`}
target='_blank'
>
<span className='text-text-accent system-xs-regular text-xs'>
{t('workflow.nodes.agent.installPlugin.changelog')}
</span>
<RiExternalLinkLine className='text-text-accent size-3' />
</Link>
}
/>}
{pluginDetail && <PluginVersionPicker
isShow={isShow}
onShowChange={setIsShow}
pluginID={pluginId}
currentVersion={pluginDetail.version}
onSelect={(state) => {
setTarget({
pluginUniqueIden: state.unique_identifier,
version: state.version,
})
showUpdateModal()
}}
trigger={
<Badge
className={cn(
'mx-1 hover:bg-state-base-hover flex',
isShow && 'bg-state-base-hover',
)}
uppercase={true}
text={
<>
<div>{pluginDetail.version}</div>
<RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' />
</>
}
hasRedCornerMark={true}
/>
}
/>}
</div>
</Tooltip>
}

View File

@ -19,7 +19,7 @@ import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import { isExceptionVariable } from '@/app/components/workflow/utils'
type VariableTagProps = {
interface VariableTagProps {
valueSelector: ValueSelector
varType: VarType
isShort?: boolean

View File

@ -11,7 +11,7 @@ import type { VarType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
import Toast from '@/app/components/base/toast'
type Props = {
interface Props {
readonly: boolean
outputs: OutputVar
outputKeyOrders: string[]

View File

@ -32,6 +32,7 @@ import {
} from '@/app/components/workflow/constants'
import type { PromptItem } from '@/models/debug'
import { VAR_REGEX } from '@/config'
import type { AgentNodeType } from '../../../agent/types'
export const isSystemVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
@ -235,7 +236,29 @@ const formatItem = (
}
case BlockEnum.Tool: {
res.vars = TOOL_OUTPUT_STRUCT
const {
output_schema,
} = data as ToolNodeType
if (!output_schema) {
res.vars = TOOL_OUTPUT_STRUCT
}
else {
const outputSchema: any[] = []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
outputSchema.push({
variable: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
})
res.vars = [
...TOOL_OUTPUT_STRUCT,
...outputSchema,
]
}
break
}
@ -293,6 +316,25 @@ const formatItem = (
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
const outputs: Var[] = []
Object.keys(payload.output_schema?.properties || {}).forEach((outputKey) => {
const output = payload.output_schema.properties[outputKey]
outputs.push({
variable: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]` as VarType
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}` as VarType,
})
})
res.vars = [
...outputs,
...TOOL_OUTPUT_STRUCT,
]
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {
@ -678,7 +720,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
break
}
case BlockEnum.LLM: {
const payload = (data as LLMNodeType)
const payload = data as LLMNodeType
const isChatModel = payload.model?.mode === 'chat'
let prompts: string[] = []
if (isChatModel) {
@ -716,19 +758,19 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
break
}
case BlockEnum.QuestionClassifier: {
const payload = (data as QuestionClassifierNodeType)
const payload = data as QuestionClassifierNodeType
res = [payload.query_variable_selector]
const varInInstructions = matchNotSystemVars([payload.instruction || ''])
res.push(...varInInstructions)
break
}
case BlockEnum.HttpRequest: {
const payload = (data as HttpNodeType)
const payload = data as HttpNodeType
res = matchNotSystemVars([payload.url, payload.headers, payload.params, typeof payload.body.data === 'string' ? payload.body.data : payload.body.data.map(d => d.value).join('')])
break
}
case BlockEnum.Tool: {
const payload = (data as ToolNodeType)
const payload = data as ToolNodeType
const mixVars = matchNotSystemVars(Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type === ToolVarType.mixed).map(key => payload.tool_parameters[key].value) as string[])
const vars = Object.keys(payload.tool_parameters).filter(key => payload.tool_parameters[key].type === ToolVarType.variable).map(key => payload.tool_parameters[key].value as string) || []
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
@ -746,7 +788,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
}
case BlockEnum.ParameterExtractor: {
const payload = (data as ParameterExtractorNodeType)
const payload = data as ParameterExtractorNodeType
res = [payload.query]
const varInInstructions = matchNotSystemVars([payload.instruction || ''])
res.push(...varInInstructions)
@ -762,6 +804,21 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
res = [(data as ListFilterNodeType).variable]
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
const valueSelectors: ValueSelector[] = []
if (!payload.agent_parameters)
break
Object.keys(payload.agent_parameters || {}).forEach((key) => {
const { value } = payload.agent_parameters![key]
if (typeof value === 'string')
valueSelectors.push(...matchNotSystemVars([value]))
})
res = valueSelectors
break
}
}
return res || []
}
@ -773,7 +830,7 @@ export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSe
let res: string | string[] = ''
switch (type) {
case BlockEnum.LLM: {
const payload = (data as LLMNodeType)
const payload = data as LLMNodeType
res = [`#${valueSelector.join('.')}#`]
if (payload.context?.variable_selector.join('.') === valueSelector.join('.'))
res.push('#context#')

View File

@ -17,7 +17,7 @@ import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others
import { checkKeys } from '@/utils/var'
import { FILE_STRUCT } from '@/app/components/workflow/constants'
type ObjectChildrenProps = {
interface ObjectChildrenProps {
nodeId: string
title: string
data: Var[]
@ -28,7 +28,7 @@ type ObjectChildrenProps = {
isSupportFileVar?: boolean
}
type ItemProps = {
interface ItemProps {
nodeId: string
title: string
objPath: string[]
@ -134,7 +134,7 @@ const Item: FC<ItemProps> = ({
zIndex: 100,
}}>
{(isObj && !isFile) && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
// eslint-disable-next-line ts/no-use-before-define
<ObjectChildren
nodeId={nodeId}
title={title}
@ -147,7 +147,7 @@ const Item: FC<ItemProps> = ({
/>
)}
{isFile && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
// eslint-disable-next-line ts/no-use-before-define
<ObjectChildren
nodeId={nodeId}
title={title}
@ -226,7 +226,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
)
}
type Props = {
interface Props {
hideSearch?: boolean
searchBoxClassName?: string
vars: NodeOutPutVar[]

View File

@ -46,6 +46,7 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
const { checkValid: checkIterationValid } = IterationDefault
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.LLM]: checkLLMValid,
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
@ -144,7 +145,7 @@ const useOneStepRun = <T>({
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([])
useEffect(() => {
if (!checkValid) {
@ -175,7 +176,7 @@ const useOneStepRun = <T>({
const workflowStore = useWorkflowStore()
useEffect(() => {
workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun)
}, [isShowSingleRun])
}, [isShowSingleRun, workflowStore])
const hideSingleRun = () => {
handleNodeDataUpdate({
@ -213,7 +214,7 @@ const useOneStepRun = <T>({
}
else {
setIterationRunResult([])
let _iterationResult: NodeTracing[][] = []
let _iterationResult: NodeTracing[] = []
let _runResult: any = null
ssePost(
getIterationSingleNodeRunUrl(isChatMode, appId!, id),
@ -233,27 +234,43 @@ const useOneStepRun = <T>({
_runResult.created_by = iterationData.created_by.name
setRunResult(_runResult)
},
onIterationNext: () => {
// iteration next trigger time is triggered one more time than iterationTimes
if (_iterationResult.length >= iterationTimes!)
return
onIterationStart: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft.push([])
draft.push({
...params.data,
status: NodeRunningStatus.Running,
})
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onIterationNext: () => {
// iteration next trigger time is triggered one more time than iterationTimes
if (_iterationResult.length >= iterationTimes!)
return _iterationResult.length >= iterationTimes!
},
onIterationFinish: (params) => {
_runResult = params.data
setRunResult(_runResult)
const iterationRunResult = _iterationResult
const currentIndex = iterationRunResult.findIndex(trace => trace.id === params.data.id)
const newIterationRunResult = produce(iterationRunResult, (draft) => {
if (currentIndex > -1) {
draft[currentIndex] = {
...draft[currentIndex],
...data,
}
}
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onNodeStarted: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft[draft.length - 1].push({
draft.push({
...params.data,
status: NodeRunningStatus.Running,
} as NodeTracing)
})
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
@ -262,18 +279,25 @@ const useOneStepRun = <T>({
const iterationRunResult = _iterationResult
const { data } = params
const currentIndex = iterationRunResult[iterationRunResult.length - 1].findIndex(trace => trace.node_id === data.node_id)
const currentIndex = iterationRunResult.findIndex(trace => trace.id === data.id)
const newIterationRunResult = produce(iterationRunResult, (draft) => {
if (currentIndex > -1) {
draft[draft.length - 1][currentIndex] = {
draft[currentIndex] = {
...draft[currentIndex],
...data,
status: NodeRunningStatus.Succeeded,
} as NodeTracing
}
}
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onNodeRetry: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft.push(params.data)
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onError: () => {
handleNodeDataUpdate({
id,

View File

@ -0,0 +1,75 @@
import Tooltip from '@/app/components/base/tooltip'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import Indicator from '@/app/components/header/indicator'
import { type FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export type ModelBarProps = {
provider: string
model: string
} | {}
const useAllModel = () => {
const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration)
const { data: moderation } = useModelList(ModelTypeEnum.moderation)
const { data: rerank } = useModelList(ModelTypeEnum.rerank)
const { data: speech2text } = useModelList(ModelTypeEnum.speech2text)
const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding)
const { data: tts } = useModelList(ModelTypeEnum.tts)
const models = useMemo(() => {
return textGeneration
.concat(moderation)
.concat(rerank)
.concat(speech2text)
.concat(textEmbedding)
.concat(tts)
}, [textGeneration, moderation, rerank, speech2text, textEmbedding, tts])
if (!textGeneration || !moderation || !rerank || !speech2text || !textEmbedding || !tts)
return undefined
return models
}
export const ModelBar: FC<ModelBarProps> = (props) => {
const { t } = useTranslation()
const modelList = useAllModel()
if (!('provider' in props)) {
return <Tooltip
popupContent={t('workflow.nodes.agent.modelNotSelected')}
triggerMethod='hover'
>
<div className='relative'>
<ModelSelector
modelList={[]}
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
defaultModel={undefined}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName='opacity-50'
/>
<Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />
</div>
</Tooltip>
}
const modelInstalled = modelList?.some(
provider => provider.provider === props.provider && provider.models.some(model => model.model === props.model))
const showWarn = modelList && !modelInstalled
return modelList && <Tooltip
popupContent={t('workflow.nodes.agent.modelNotInstallTooltip')}
triggerMethod='hover'
disabled={!modelList || modelInstalled}
>
<div className='relative'>
<ModelSelector
modelList={modelList}
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
defaultModel={props}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName='opacity-50'
/>
{showWarn && <Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />}
</div>
</Tooltip>
}

View File

@ -0,0 +1,81 @@
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { memo, useMemo, useRef, useState } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { useTranslation } from 'react-i18next'
import { Group } from '@/app/components/base/icons/src/vender/other'
type Status = 'not-installed' | 'not-authorized' | undefined
export type ToolIconProps = {
providerName: string
}
export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName
})
}, [buildInTools, customTools, providerName, workflowTools])
const providerNameParts = providerName.split('/')
const author = providerNameParts[0]
const name = providerNameParts[1]
const icon = useMemo(() => {
if (currentProvider) return currentProvider.icon as string
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
return iconFromMarketPlace
}, [author, currentProvider, name])
const status: Status = useMemo(() => {
if (!isDataReady) return undefined
if (!currentProvider) return 'not-installed'
if (currentProvider.is_team_authorization === false) return 'not-authorized'
return undefined
}, [currentProvider, isDataReady])
const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
const { t } = useTranslation()
const tooltip = useMemo(() => {
if (!notSuccess) return undefined
if (status === 'not-installed') return t('workflow.nodes.agent.toolNotInstallTooltip', { tool: name })
if (status === 'not-authorized') return t('workflow.nodes.agent.toolNotAuthorizedTooltip', { tool: name })
throw new Error('Unknown status')
}, [name, notSuccess, status, t])
const [iconFetchError, setIconFetchError] = useState(false)
return <Tooltip
triggerMethod='hover'
popupContent={tooltip}
disabled={!notSuccess}
>
<div
className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
)}
ref={containerRef}
>
{!iconFetchError
// eslint-disable-next-line @next/next/no-img-element
? <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
: <Group className="w-3 h-3 opacity-35" />
}
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
</div>
</Tooltip>
})
ToolIcon.displayName = 'ToolIcon'

View File

@ -0,0 +1,54 @@
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '../../constants'
import type { NodeDefault } from '../../types'
import type { AgentNodeType } from './types'
import { renderI18nObject } from '@/hooks/use-i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
getAvailableNextNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
checkValid(payload, t, moreDataForCheckValid: {
strategyProvider?: StrategyPluginDetail,
strategy?: StrategyDetail
language: string
isReadyForCheckValid: boolean
}) {
const { strategy, language, isReadyForCheckValid } = moreDataForCheckValid
if (!isReadyForCheckValid) {
return {
isValid: true,
errorMessage: '',
}
}
if (!strategy) {
return {
isValid: false,
errorMessage: t('workflow.nodes.agent.checkList.strategyNotSelected'),
}
}
for (const param of strategy.parameters) {
if (param.required && !payload.agent_parameters?.[param.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
}
return {
isValid: true,
errorMessage: '',
}
},
}
export default nodeDefault

View File

@ -0,0 +1,113 @@
import { type FC, memo, useMemo } from 'react'
import type { NodeProps } from '../../types'
import type { AgentNodeType } from './types'
import { SettingItem } from '../_base/components/setting-item'
import { Group, GroupLabel } from '../_base/components/group'
import type { ToolIconProps } from './components/tool-icon'
import { ToolIcon } from './components/tool-icon'
import useConfig from './use-config'
import { useTranslation } from 'react-i18next'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { ModelBar } from './components/model-bar'
const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const { inputs, currentStrategy, currentStrategyStatus, pluginDetail } = useConfig(props.id, props.data)
const renderI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const models = useMemo(() => {
if (!inputs) return []
// if selected, show in node
// if required and not selected, show empty selector
// if not required and not selected, show nothing
const models = currentStrategy?.parameters
.filter(param => param.type === FormTypeEnum.modelSelector)
.reduce((acc, param) => {
const item = inputs.agent_parameters?.[param.name]?.value
if (!item) {
if (param.required) {
acc.push({ param: param.name })
return acc
}
else { return acc }
}
acc.push({ provider: item.provider, model: item.model, param: param.name })
return acc
}, [] as Array<{ param: string } | { provider: string, model: string, param: string }>) || []
return models
}, [currentStrategy, inputs])
const tools = useMemo(() => {
const tools: Array<ToolIconProps> = []
currentStrategy?.parameters.forEach((param) => {
if (param.type === FormTypeEnum.toolSelector) {
const field = param.name
const value = inputs.agent_parameters?.[field]?.value
if (value) {
tools.push({
providerName: value.provider_name as any,
})
}
}
if (param.type === FormTypeEnum.multiToolSelector) {
const field = param.name
const value = inputs.agent_parameters?.[field]?.value
if (value) {
(value as unknown as any[]).forEach((item) => {
tools.push({
providerName: item.provider_name,
})
})
}
}
})
return tools
}, [currentStrategy?.parameters, inputs.agent_parameters])
return <div className='mb-1 px-3 py-1 space-y-1'>
{inputs.agent_strategy_name
? <SettingItem
label={t('workflow.nodes.agent.strategy.shortLabel')}
status={
currentStrategyStatus && !currentStrategyStatus.isExistInPlugin
? 'error'
: undefined
}
tooltip={
(currentStrategyStatus && !currentStrategyStatus.isExistInPlugin)
? t('workflow.nodes.agent.strategyNotInstallTooltip', {
plugin: pluginDetail?.declaration.label
? renderI18nObject(pluginDetail?.declaration.label)
: undefined,
strategy: inputs.agent_strategy_label,
})
: undefined
}
>
{inputs.agent_strategy_label}
</SettingItem>
: <SettingItem label={t('workflow.nodes.agent.strategyNotSet')} />}
{models.length > 0 && <Group
label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.model')}
</GroupLabel>}
>
{models.map((model) => {
return <ModelBar
{...model}
key={model.param}
/>
})}
</Group>}
{tools.length > 0 && <Group label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.toolbox')}
</GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'>
{tools.map(tool => <ToolIcon {...tool} key={Math.random()} />)}
</div>
</Group>}
</div>
}
AgentNode.displayName = 'AgentNode'
export default memo(AgentNode)

View File

@ -0,0 +1,154 @@
import type { FC } from 'react'
import { memo, useMemo } from 'react'
import type { NodePanelProps } from '../../types'
import type { AgentNodeType } from './types'
import Field from '../_base/components/field'
import { AgentStrategy } from '../_base/components/agent-strategy'
import useConfig from './use-config'
import { useTranslation } from 'react-i18next'
import OutputVars, { VarItem } from '../_base/components/output-vars'
import type { StrategyParamItem } from '@/app/components/plugins/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import { useLogs } from '@/app/components/workflow/run/hooks'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import { toType } from '@/app/components/tools/utils/to-form-schema'
import { useStore } from '../../store'
const i18nPrefix = 'workflow.nodes.agent'
export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
return {
...param as any,
variable: param.name,
show_on: [],
type: toType(param.type),
}
}
const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const {
inputs,
setInputs,
currentStrategy,
formData,
onFormChange,
availableNodesWithParent,
availableVars,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
runInputData,
setRunInputData,
varInputs,
} = useConfig(props.id, props.data)
const { t } = useTranslation()
const nodeInfo = useMemo(() => {
if (!runResult)
return
return formatTracing([runResult], t)[0]
}, [runResult, t])
const logsParams = useLogs()
const singleRunForms = (() => {
const forms: FormProps[] = []
if (varInputs.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.singleRun.variable`)!,
inputs: varInputs,
values: runInputData,
onChange: setRunInputData,
},
)
}
return forms
})()
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4 py-2' tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<AgentStrategy
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
} : undefined}
onStrategyChange={(strategy) => {
setInputs({
...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name,
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
agent_parameters: {},
})
resetEditor(Date.now())
}}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={formData}
onFormValueChange={onFormChange}
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
</Field>
<div>
<OutputVars>
<VarItem
name='text'
type='String'
description={t(`${i18nPrefix}.outputVars.text`)}
/>
<VarItem
name='files'
type='Array[File]'
description={t(`${i18nPrefix}.outputVars.files.title`)}
/>
<VarItem
name='json'
type='Array[Object]'
description={t(`${i18nPrefix}.outputVars.json`)}
/>
{inputs.output_schema && Object.entries(inputs.output_schema).map(([name, schema]) => (
<VarItem
key={name}
name={name}
type={schema.type}
description={schema.description}
/>
))}
</OutputVars>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={<ResultPanel {...runResult} nodeInfo={nodeInfo} showSteps={false} {...logsParams} />}
/>
)
}
</div>
}
AgentPanel.displayName = 'AgentPanel'
export default memo(AgentPanel)

View File

@ -0,0 +1,11 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ToolVarInputs } from '../tool/types'
export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string
agent_strategy_name?: string
agent_strategy_label?: string
agent_parameters?: ToolVarInputs
output_schema: Record<string, any>
plugin_unique_identifier?: string
}

View File

@ -0,0 +1,190 @@
import { useStrategyProviderDetail } from '@/service/use-strategy'
import useNodeCrud from '../_base/hooks/use-node-crud'
import useVarList from '../_base/hooks/use-var-list'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { AgentNodeType } from './types'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useCallback, useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import type { Var } from '../../types'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
export type StrategyStatus = {
plugin: {
source: 'external' | 'marketplace'
installed: boolean
}
isExistInPlugin: boolean
}
export const useStrategyInfo = (
strategyProviderName?: string,
strategyName?: string,
) => {
const strategyProvider = useStrategyProviderDetail(
strategyProviderName || '',
{ retry: false },
)
const strategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === strategyName,
)
const marketplace = useFetchPluginsInMarketPlaceByIds([strategyProviderName!], {
retry: false,
})
const strategyStatus: StrategyStatus | undefined = useMemo(() => {
if (strategyProvider.isLoading || marketplace.isLoading)
return undefined
const strategyExist = !!strategy
const isPluginInstalled = !strategyProvider.isError
const isInMarketplace = !!marketplace.data?.data.plugins.at(0)
return {
plugin: {
source: isInMarketplace ? 'marketplace' : 'external',
installed: isPluginInstalled,
},
isExistInPlugin: strategyExist,
}
}, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading])
const refetch = useCallback(() => {
strategyProvider.refetch()
marketplace.refetch()
}, [marketplace, strategyProvider])
return {
strategyProvider,
strategy,
strategyStatus,
refetch,
}
}
const useConfig = (id: string, payload: AgentNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<AgentNodeType>(id, payload)
// variables
const { handleVarListChange, handleAddVariable } = useVarList<AgentNodeType>({
inputs,
setInputs,
})
const {
strategyStatus: currentStrategyStatus,
strategy: currentStrategy,
strategyProvider,
} = useStrategyInfo(
inputs.agent_strategy_provider_name,
inputs.agent_strategy_name,
)
const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/')
const pluginDetail = useCheckInstalled({
pluginIds: [pluginId!],
enabled: Boolean(pluginId),
})
const formData = useMemo(() => {
return Object.fromEntries(
Object.entries(inputs.agent_parameters || {}).map(([key, value]) => {
return [key, value.value]
}),
)
}, [inputs.agent_parameters])
const onFormChange = (value: Record<string, any>) => {
const res: ToolVarInputs = {}
Object.entries(value).forEach(([key, val]) => {
res[key] = {
type: VarType.constant,
value: val,
}
})
setInputs({
...inputs,
agent_parameters: res,
})
}
// vars
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
return [
VarKindType.arrayObject,
VarKindType.array,
VarKindType.number,
VarKindType.string,
VarKindType.secret,
VarKindType.arrayString,
VarKindType.arrayNumber,
VarKindType.file,
VarKindType.arrayFile,
].includes(varPayload.type)
}, [])
const {
availableVars,
availableNodesWithParent,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterMemoryPromptVar,
})
// single run
const {
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
getInputVars,
} = useOneStepRun<AgentNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})
const allVarStrArr = (() => {
const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => {
return formData[item.name]
}) || []
return arr
})()
const varInputs = (() => {
const vars = getInputVars(allVarStrArr)
return vars
})()
return {
readOnly,
inputs,
setInputs,
handleVarListChange,
handleAddVariable,
currentStrategy,
formData,
onFormChange,
currentStrategyStatus,
strategyProvider: strategyProvider.data,
pluginDetail: pluginDetail.data?.plugins.at(0),
availableVars,
availableNodesWithParent,
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
varInputs,
}
}
export default useConfig

View File

@ -108,7 +108,7 @@ const OperationSelector: FC<OperationSelectorProps> = ({
}}
>
<div className='flex min-h-5 px-1 items-center gap-1 grow'>
<span className={'flex flex-grow text-text-secondary system-sm-medium'}>{t(`${i18nPrefix}.operations.${item.name}`)}</span>
<span className={'flex grow text-text-secondary system-sm-medium'}>{t(`${i18nPrefix}.operations.${item.name}`)}</span>
</div>
{item.value === value && (
<div className='flex justify-center items-center'>

View File

@ -128,7 +128,7 @@ const VarList: FC<Props> = ({
return (
<div className='flex items-start gap-1 self-stretch' key={index}>
<div className='flex flex-col items-start gap-1 flex-grow'>
<div className='flex flex-col items-start gap-1 grow'>
<div className='flex items-center gap-1 self-stretch'>
<VarReferencePicker
readonly={readonly}
@ -212,7 +212,7 @@ const VarList: FC<Props> = ({
</div>
<ActionButton
size='l'
className='flex-shrink-0 group hover:!bg-state-destructive-hover'
className='shrink-0 group hover:!bg-state-destructive-hover'
onClick={handleVarRemove(index)}
>
<RiDeleteBinLine className='text-text-tertiary w-4 h-4 group-hover:text-text-destructive' />

View File

@ -3,7 +3,7 @@ import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type AssignerNodeType } from './types'
import type { AssignerNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'

View File

@ -7,9 +7,9 @@ import {
import VarList from './components/var-list'
import useConfig from './use-config'
import type { AssignerNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { useHandleAddOperationItem } from './hooks'
import ActionButton from '@/app/components/base/action-button'
import { type NodePanelProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.assigner'
@ -40,7 +40,7 @@ const Panel: FC<NodePanelProps<AssignerNodeType>> = ({
<div className='flex py-2 flex-col items-start self-stretch'>
<div className='flex flex-col justify-center items-start gap-1 px-4 py-2 w-full self-stretch'>
<div className='flex items-start gap-2 self-stretch'>
<div className='flex flex-col justify-center items-start flex-grow text-text-secondary system-sm-semibold-uppercase'>{t(`${i18nPrefix}.variables`)}</div>
<div className='flex flex-col justify-center items-start grow text-text-secondary system-sm-semibold-uppercase'>{t(`${i18nPrefix}.variables`)}</div>
<ActionButton onClick={handleAddOperation}>
<RiAddLine className='w-4 h-4 shrink-0 text-text-tertiary' />
</ActionButton>

View File

@ -13,7 +13,7 @@ import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
const i18nPrefix = 'workflow.nodes.code'

View File

@ -34,6 +34,8 @@ import DocExtractorNode from './document-extractor/node'
import DocExtractorPanel from './document-extractor/panel'
import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
@ -54,6 +56,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
@ -75,6 +78,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationPanel,
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
}
export const CUSTOM_NODE_TYPE = 'custom'

View File

@ -1,6 +1,6 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'

View File

@ -3,7 +3,7 @@ import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'

View File

@ -4,7 +4,7 @@ import { useStoreApi } from 'reactflow'
import type { ValueSelector, Var } from '../../types'
import { InputVarType, VarType } from '../../types'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import {

View File

@ -1,6 +1,6 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type EndNodeType } from './types'
import type { EndNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const nodeDefault: NodeDefault<EndNodeType> = {

View File

@ -1,4 +1,4 @@
import { type FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import useConfig from './use-config'
@ -6,7 +6,7 @@ import type { EndNodeType } from './types'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import AddButton from '@/app/components/base/button/add-button'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.end'

View File

@ -17,7 +17,7 @@ import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http.authorization'
type Props = {
interface Props {
nodeId: string
payload: AuthorizationPayloadType
onChange: (payload: AuthorizationPayloadType) => void

View File

@ -15,7 +15,7 @@ import { VarType } from '@/app/components/workflow/types'
const UNIQUE_ID_PREFIX = 'key-value-'
type Props = {
interface Props {
readonly: boolean
nodeId: string
payload: Body

View File

@ -4,7 +4,7 @@ import React from 'react'
import type { KeyValue } from '../../types'
import KeyValueEdit from './key-value-edit'
type Props = {
interface Props {
readonly: boolean
nodeId: string
list: KeyValue[]

View File

@ -9,7 +9,7 @@ import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
interface Props {
readonly: boolean
nodeId: string
list: KeyValue[]

View File

@ -8,7 +8,7 @@ import RemoveButton from '@/app/components/workflow/nodes/_base/components/remov
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
interface Props {
className?: string
instanceId?: string
nodeId: string

View File

@ -14,7 +14,7 @@ import { VarType } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
interface Props {
instanceId: string
className?: string
nodeId: string

View File

@ -6,7 +6,7 @@ import type { Timeout as TimeoutPayloadType } from '../../types'
import Input from '@/app/components/base/input'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
type Props = {
interface Props {
readonly: boolean
nodeId: string
payload: TimeoutPayloadType
@ -35,7 +35,7 @@ const InputField: FC<{
type='number'
value={value}
onChange={(e) => {
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
const value = Math.max(min, Math.min(max, Number.parseInt(e.target.value, 10)))
onChange(value)
}}
placeholder={placeholder}

View File

@ -18,7 +18,6 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.http'
@ -61,10 +60,6 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
// To prevent prompt editor in body not update data.
if (!isDataReady)
return null
@ -198,9 +193,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
{(isShowCurlPanel && !readOnly) && (

View File

@ -18,7 +18,7 @@ export enum BodyType {
binary = 'binary',
}
export type KeyValue = {
export interface KeyValue {
id?: string
key: string
value: string
@ -38,7 +38,7 @@ export type BodyPayload = {
file?: ValueSelector // when type is file
value?: string // when type is text
}[]
export type Body = {
export interface Body {
type: BodyType
data: string | BodyPayload // string is deprecated, it would convert to BodyPayload after loaded
}
@ -54,7 +54,7 @@ export enum APIType {
custom = 'custom',
}
export type Authorization = {
export interface Authorization {
type: AuthorizationType
config?: {
type: APIType
@ -63,7 +63,7 @@ export type Authorization = {
} | null
}
export type Timeout = {
export interface Timeout {
connect?: number
read?: number
write?: number

View File

@ -18,7 +18,7 @@ import type {
Var,
} from '@/app/components/workflow/types'
type ConditionAddProps = {
interface ConditionAddProps {
className?: string
caseId: string
variables: NodeOutPutVar[]

View File

@ -39,7 +39,7 @@ import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
type ConditionItemProps = {
interface ConditionItemProps {
className?: string
disabled?: boolean
caseId: string

View File

@ -16,7 +16,7 @@ import type { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.ifElse'
type ConditionOperatorProps = {
interface ConditionOperatorProps {
className?: string
disabled?: boolean
varType: VarType

View File

@ -19,7 +19,7 @@ import type {
} from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type ConditionListProps = {
interface ConditionListProps {
isSubVariable?: boolean
disabled?: boolean
caseId: string

View File

@ -30,7 +30,7 @@ const options = [
NumberVarType.constant,
]
type ConditionNumberInputProps = {
interface ConditionNumberInputProps {
numberVarType?: NumberVarType
onNumberVarTypeChange: (v: NumberVarType) => void
value: string

View File

@ -20,7 +20,7 @@ import type {
Node,
} from '@/app/components/workflow/types'
type ConditionValueProps = {
interface ConditionValueProps {
variableSelector: string[]
labelName?: string
operator: ComparisonOperator

View File

@ -35,7 +35,7 @@ export enum ComparisonOperator {
notExists = 'not exists',
}
export type Condition = {
export interface Condition {
id: string
varType: VarType
variable_selector?: ValueSelector
@ -46,7 +46,7 @@ export type Condition = {
sub_variable_condition?: CaseItem
}
export type CaseItem = {
export interface CaseItem {
case_id: string
logical_operator: LogicalOperator
conditions: Condition[]

View File

@ -1,13 +1,9 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
} from '@remixicon/react'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import Split from '../_base/components/split'
import ResultPanel from '../../run/result-panel'
import IterationResultPanel from '../../run/iteration-result-panel'
import { MAX_ITERATION_PARALLEL_NUM, MIN_ITERATION_PARALLEL_NUM } from '../../constants'
import type { IterationNodeType } from './types'
import useConfig from './use-config'
@ -18,6 +14,9 @@ import Switch from '@/app/components/base/switch'
import Select from '@/app/components/base/select'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import { useLogs } from '@/app/components/workflow/run/hooks'
const i18nPrefix = 'workflow.nodes.iteration'
@ -50,10 +49,6 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
handleOutputVarChange,
isShowSingleRun,
hideSingleRun,
isShowIterationDetail,
backToSingleRun,
showIterationDetail,
hideIterationDetail,
runningStatus,
handleRun,
handleStop,
@ -70,6 +65,9 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
changeParallelNums,
} = useConfig(id, data)
const nodeInfo = formatTracing(iterationRunResult, t)[0]
const logsParams = useLogs()
return (
<div className='pt-2 pb-2'>
<div className='px-4 pb-4 space-y-4'>
@ -123,7 +121,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
onChange={changeParallelNums}
max={MAX_ITERATION_PARALLEL_NUM}
min={MIN_ITERATION_PARALLEL_NUM}
className=' flex-shrink-0 flex-1 mt-4'
className=' shrink-0 flex-1 mt-4'
/>
</div>
@ -164,27 +162,12 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={
<div className='mt-3'>
<div className='px-4'>
<div className='flex items-center h-[34px] justify-between px-3 bg-gray-100 border-[0.5px] border-gray-200 rounded-lg cursor-pointer' onClick={showIterationDetail}>
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>{t(`${i18nPrefix}.iteration`, { count: iterationRunResult.length })}</div>
<RiArrowRightSLine className='w-3.5 h-3.5 text-gray-500' />
</div>
<Split className='mt-3' />
</div>
<ResultPanel {...runResult} showSteps={false} />
</div>
<ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} />
}
/>
)}
{isShowIterationDetail && (
<IterationResultPanel
onBack={backToSingleRun}
onHide={hideIterationDetail}
list={iterationRunResult}
/>
)}
</div>
)
}

View File

@ -18,7 +18,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Badge from '@/app/components/base/badge'
import { useKnowledge } from '@/hooks/use-knowledge'
type Props = {
interface Props {
payload: DataSet
onRemove: () => void
onChange: (dataSet: DataSet) => void

View File

@ -45,7 +45,7 @@ const LimitConfig: FC<Props> = ({
const handleLimitSizeChange = useCallback((size: number | string) => {
onChange({
...config,
size: parseInt(size as string),
size: Number.parseInt(size as string),
})
}, [onChange, config])

View File

@ -3,7 +3,7 @@ import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type ListFilterNodeType } from './types'
import type { ListFilterNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'

View File

@ -11,7 +11,7 @@ import { type ListFilterNodeType, OrderBy } from './types'
import LimitConfig from './components/limit-config'
import FilterCondition from './components/filter-condition'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Switch from '@/app/components/base/switch'
import ExtractInput from '@/app/components/workflow/nodes/list-operator/components/extract-input'

View File

@ -25,6 +25,7 @@ const Node: FC<NodeProps<LLMNodeType>> = ({
<ModelSelector
defaultModel={{ provider, model: modelId }}
modelList={textGenerationModelList}
triggerClassName='!h-6 !rounded-md'
readonly
/>
)}

View File

@ -19,7 +19,6 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c
import ResultPanel from '@/app/components/workflow/run/result-panel'
import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.llm'
@ -70,10 +69,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
runResult,
filterJinjia2InputVar,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
const model = inputs.model
@ -293,9 +288,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>

View File

@ -28,7 +28,7 @@ const DEFAULT_PARAM: Param = {
required: false,
}
type Props = {
interface Props {
type: 'add' | 'edit'
payload?: Param
onSave: (payload: Param, moreInfo?: MoreInfo) => void

View File

@ -8,7 +8,7 @@ import OptionCard from '../../_base/components/option-card'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
type Props = {
interface Props {
type: ReasoningModeType
onChange: (type: ReasoningModeType) => void
}

View File

@ -21,6 +21,7 @@ const Node: FC<NodeProps<ParameterExtractorNodeType>> = ({
<ModelSelector
defaultModel={{ provider, model: modelId }}
modelList={textGenerationModelList}
triggerClassName='!h-6 !rounded-md'
readonly
/>
)}

View File

@ -10,7 +10,7 @@ export enum ParamType {
arrayObject = 'array[object]',
}
export type Param = {
export interface Param {
name: string
type: ParamType
options?: string[]

View File

@ -32,6 +32,7 @@ const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
{hasSetModel && (
<ModelSelector
defaultModel={{ provider, model: modelId }}
triggerClassName='!h-6 !rounded-md'
modelList={textGenerationModelList}
readonly
/>

View File

@ -1,6 +1,6 @@
import type { CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting } from '@/app/components/workflow/types'
export type Topic = {
export interface Topic {
id: string
name: string
}

View File

@ -13,7 +13,7 @@ import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
import Badge from '@/app/components/base/badge'
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
type Props = {
interface Props {
readonly: boolean
payload: InputVar
onChange?: (item: InputVar, moreInfo?: MoreInfo) => void

View File

@ -14,6 +14,9 @@ import VarReferencePicker from '@/app/components/workflow/nodes/_base/components
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { VarType } from '@/app/components/workflow/types'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
type Props = {
readOnly: boolean
nodeId: string
@ -46,12 +49,14 @@ const InputVarList: FC<Props> = ({
const paramType = (type: string) => {
if (type === FormTypeEnum.textNumber)
return 'Number'
else if (type === FormTypeEnum.file)
return 'File'
else if (type === FormTypeEnum.files)
else if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return 'Files'
else if (type === FormTypeEnum.select)
return 'Options'
else if (type === FormTypeEnum.appSelector)
return 'AppSelector'
else if (type === FormTypeEnum.modelSelector)
return 'ModelSelector'
else if (type === FormTypeEnum.toolSelector)
return 'ToolSelector'
else
return 'String'
}
@ -73,7 +78,7 @@ const InputVarList: FC<Props> = ({
})
onChange(newValue)
}
}, [value, onChange, isSupportConstantValue])
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
@ -105,6 +110,30 @@ const InputVarList: FC<Props> = ({
}
}, [value, onChange])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable] = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable] = {
...draft[variable],
...model,
} as any
})
onChange(newValue)
}
}, [onChange, value])
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
@ -129,13 +158,16 @@ const InputVarList: FC<Props> = ({
type,
required,
tooltip,
scope,
} = schema
const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file
const isFileArray = type === FormTypeEnum.files
const isString = !isNumber && !isSelect && !isFile && !isFileArray
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
return (
<div key={variable} className='space-y-1'>
@ -181,19 +213,26 @@ const InputVarList: FC<Props> = ({
onChange={handleFileChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
/>
)}
{isFileArray && (
<VarReferencePicker
{isAppSelector && (
<AppSelector
disabled={readOnly}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput as any}
setModel={handleModelChange(variable)}
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.arrayFile}
scope={scope}
/>
)}
{tooltip && <div className='text-text-tertiary body-xs-regular'>{tooltip[language] || tooltip.en_US}</div>}

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import React from 'react'
import type { ToolNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const Node: FC<NodeProps<ToolNodeType>> = ({
data,
@ -20,9 +21,21 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium text-gray-500 uppercase'>
{key}
</div>
<div title={tool_configurations[key]} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'>
{tool_configurations[key]}
</div>
{typeof tool_configurations[key] === 'string' && (
<div title={tool_configurations[key]} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'>
{tool_configurations[key]}
</div>
)}
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
<div title={tool_configurations[key].model} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'>
{tool_configurations[key].model}
</div>
)}
{/* {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.appSelector && (
<div title={tool_configurations[key].app_id} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'>
{tool_configurations[key].app_id}
</div>
)} */}
</div>
))}

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Split from '../_base/components/split'
import type { ToolNodeType } from './types'
@ -14,8 +14,9 @@ import Loading from '@/app/components/base/loading'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
import { useToolIcon } from '@/app/components/workflow/hooks'
import { useLogs } from '@/app/components/workflow/run/hooks'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
const i18nPrefix = 'workflow.nodes.tool'
@ -49,12 +50,15 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleRun,
handleStop,
runResult,
outputSchema,
} = useConfig(id, data)
const toolIcon = useToolIcon(data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
const logsParams = useLogs()
const nodeInfo = useMemo(() => {
if (!runResult)
return null
return formatToTracingNodeList([runResult], t)[0]
}, [runResult, t])
if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'>
@ -143,6 +147,14 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
type='Array[Object]'
description={t(`${i18nPrefix}.outputVars.json`)}
/>
{outputSchema.map(outputItem => (
<VarItem
key={outputItem.name}
name={outputItem.name}
type={outputItem.type}
description={outputItem.description}
/>
))}
</>
</OutputVars>
</div>
@ -157,9 +169,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
{...logsParams}
result={<ResultPanel {...runResult} showSteps={false} {...logsParams} nodeInfo={nodeInfo} />}
/>
)}
</div>

View File

@ -9,7 +9,7 @@ export enum VarType {
export type ToolVarInputs = Record<string, {
type: VarType
value?: string | ValueSelector
value?: string | ValueSelector | any
}>
export type ToolNodeType = CommonNodeType & {
@ -20,4 +20,5 @@ export type ToolNodeType = CommonNodeType & {
tool_label: string
tool_parameters: ToolVarInputs
tool_configurations: Record<string, any>
output_schema: Record<string, any>
}

View File

@ -29,8 +29,9 @@ const useConfig = (id: string, payload: ToolNodeType) => {
/*
* tool_configurations: tool setting, not dynamic setting
* tool_parameters: tool dynamic setting(by user)
* output_schema: tool dynamic output
*/
const { provider_id, provider_type, tool_name, tool_configurations } = inputs
const { provider_id, provider_type, tool_name, tool_configurations, output_schema } = inputs
const isBuiltIn = provider_type === CollectionType.builtIn
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
@ -91,7 +92,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
const value = newConfig[key]
if (schema?.type === 'boolean') {
if (typeof value === 'string')
newConfig[key] = parseInt(value, 10)
newConfig[key] = Number.parseInt(value, 10)
if (typeof value === 'boolean')
newConfig[key] = value ? 1 : 0
@ -99,7 +100,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
if (schema?.type === 'number-input') {
if (typeof value === 'string' && value !== '')
newConfig[key] = parseFloat(value)
newConfig[key] = Number.parseFloat(value)
}
})
draft.tool_configurations = newConfig
@ -162,7 +163,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({})
const setInputVarValues = (value: Record<string, any>) => {
doSetInputVarValues(value)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
// eslint-disable-next-line ts/no-use-before-define
setRunInputData(value)
}
// fill single run form variable with constant value first time
@ -254,6 +255,23 @@ const useConfig = (id: string, payload: ToolNodeType) => {
doHandleRun(addMissedVarData)
}
const outputSchema = useMemo(() => {
const res: any[] = []
if (!output_schema)
return []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
res.push({
name: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
})
return res
}, [output_schema])
return {
readOnly,
inputs,
@ -282,6 +300,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
handleRun,
handleStop,
runResult,
outputSchema,
}
}

View File

@ -24,7 +24,7 @@ type Payload = VarGroupItemType & {
group_name?: string
}
type Props = {
interface Props {
readOnly: boolean
nodeId: string
payload: Payload

View File

@ -7,7 +7,7 @@ import useConfig from './use-config'
import type { VariableAssignerNodeType } from './types'
import VarGroupItem from './components/var-group-item'
import cn from '@/utils/classnames'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import Switch from '@/app/components/base/switch'