mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
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:
@ -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
|
||||
|
||||
@ -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'
|
||||
@ -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'
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Collapse from '.'
|
||||
|
||||
type FieldCollapseProps = {
|
||||
title: string
|
||||
children: JSX.Element
|
||||
children: ReactNode
|
||||
}
|
||||
const FieldCollapse = ({
|
||||
title,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
})()
|
||||
|
||||
@ -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>
|
||||
|
||||
25
web/app/components/workflow/nodes/_base/components/group.tsx
Normal file
25
web/app/components/workflow/nodes/_base/components/group.tsx
Normal 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>
|
||||
}
|
||||
@ -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> = ({
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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> = ({
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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'
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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#')
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
}
|
||||
@ -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'
|
||||
54
web/app/components/workflow/nodes/agent/default.ts
Normal file
54
web/app/components/workflow/nodes/agent/default.ts
Normal 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
|
||||
113
web/app/components/workflow/nodes/agent/node.tsx
Normal file
113
web/app/components/workflow/nodes/agent/node.tsx
Normal 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)
|
||||
154
web/app/components/workflow/nodes/agent/panel.tsx
Normal file
154
web/app/components/workflow/nodes/agent/panel.tsx
Normal 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)
|
||||
11
web/app/components/workflow/nodes/agent/types.ts
Normal file
11
web/app/components/workflow/nodes/agent/types.ts
Normal 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
|
||||
}
|
||||
190
web/app/components/workflow/nodes/agent/use-config.ts
Normal file
190
web/app/components/workflow/nodes/agent/use-config.ts
Normal 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
|
||||
@ -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'>
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -9,7 +9,7 @@ import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,7 @@ import type {
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionAddProps = {
|
||||
interface ConditionAddProps {
|
||||
className?: string
|
||||
caseId: string
|
||||
variables: NodeOutPutVar[]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -30,7 +30,7 @@ const options = [
|
||||
NumberVarType.constant,
|
||||
]
|
||||
|
||||
type ConditionNumberInputProps = {
|
||||
interface ConditionNumberInputProps {
|
||||
numberVarType?: NumberVarType
|
||||
onNumberVarTypeChange: (v: NumberVarType) => void
|
||||
value: string
|
||||
|
||||
@ -20,7 +20,7 @@ import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionValueProps = {
|
||||
interface ConditionValueProps {
|
||||
variableSelector: string[]
|
||||
labelName?: string
|
||||
operator: ComparisonOperator
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ const Node: FC<NodeProps<LLMNodeType>> = ({
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
modelList={textGenerationModelList}
|
||||
triggerClassName='!h-6 !rounded-md'
|
||||
readonly
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ const Node: FC<NodeProps<ParameterExtractorNodeType>> = ({
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
modelList={textGenerationModelList}
|
||||
triggerClassName='!h-6 !rounded-md'
|
||||
readonly
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -10,7 +10,7 @@ export enum ParamType {
|
||||
arrayObject = 'array[object]',
|
||||
}
|
||||
|
||||
export type Param = {
|
||||
export interface Param {
|
||||
name: string
|
||||
type: ParamType
|
||||
options?: string[]
|
||||
|
||||
@ -32,6 +32,7 @@ const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
|
||||
{hasSetModel && (
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
triggerClassName='!h-6 !rounded-md'
|
||||
modelList={textGenerationModelList}
|
||||
readonly
|
||||
/>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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>
|
||||
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ type Payload = VarGroupItemType & {
|
||||
group_name?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
payload: Payload
|
||||
|
||||
@ -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'
|
||||
|
||||
Reference in New Issue
Block a user