mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 06:58:05 +08:00
220 lines
8.9 KiB
TypeScript
220 lines
8.9 KiB
TypeScript
'use client'
|
|
import type { FC } from 'react'
|
|
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
|
|
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
|
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
|
import { memo, useCallback, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Button from '@/app/components/base/button'
|
|
import {
|
|
PortalToFollowElem,
|
|
PortalToFollowElemContent,
|
|
PortalToFollowElemTrigger,
|
|
} from '@/app/components/base/portal-to-follow-elem'
|
|
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
|
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
|
import Tab, { TabType } from '@/app/components/workflow/nodes/_base/components/workflow-panel/tab'
|
|
import { NULL_STRATEGY } from '@/app/components/workflow/nodes/_base/constants'
|
|
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
|
import { cn } from '@/utils/classnames'
|
|
|
|
type ConfigPanelProps = {
|
|
agentName: string
|
|
extractorNodeId: string
|
|
nestedNodeConfig: NestedNodeConfig
|
|
availableNodes: Node[]
|
|
availableVars: NodeOutPutVar[]
|
|
onNestedNodeConfigChange: (config: NestedNodeConfig) => void
|
|
}
|
|
|
|
const ConfigPanel: FC<ConfigPanelProps> = ({
|
|
agentName,
|
|
extractorNodeId,
|
|
nestedNodeConfig,
|
|
availableNodes,
|
|
availableVars,
|
|
onNestedNodeConfigChange,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const [tabType, setTabType] = useState<TabType>(TabType.settings)
|
|
|
|
const resolvedExtractorId = nestedNodeConfig.extractor_node_id || extractorNodeId
|
|
|
|
const selectedOutput = useMemo<ValueSelector>(() => {
|
|
if (!resolvedExtractorId || !nestedNodeConfig.output_selector?.length)
|
|
return []
|
|
|
|
return [resolvedExtractorId, ...(nestedNodeConfig.output_selector || [])]
|
|
}, [nestedNodeConfig.output_selector, resolvedExtractorId])
|
|
|
|
const handleOutputVarChange = useCallback((value: ValueSelector | string) => {
|
|
const selector = Array.isArray(value) ? value : []
|
|
const nextExtractorId = selector[0] || resolvedExtractorId
|
|
const nextOutputSelector = selector.length > 1 ? selector.slice(1) : []
|
|
|
|
onNestedNodeConfigChange({
|
|
...nestedNodeConfig,
|
|
extractor_node_id: nextExtractorId,
|
|
output_selector: nextOutputSelector,
|
|
})
|
|
}, [nestedNodeConfig, onNestedNodeConfigChange, resolvedExtractorId])
|
|
|
|
const [nullStrategyOpen, setNullStrategyOpen] = useState(false)
|
|
const whenOutputNoneOptions = useMemo(() => ([
|
|
{
|
|
value: NULL_STRATEGY.RAISE_ERROR,
|
|
label: t('subGraphModal.whenOutputNone.error', { ns: 'workflow' }),
|
|
description: t('subGraphModal.whenOutputNone.errorDesc', { ns: 'workflow' }),
|
|
},
|
|
{
|
|
value: NULL_STRATEGY.USE_DEFAULT,
|
|
label: t('subGraphModal.whenOutputNone.default', { ns: 'workflow' }),
|
|
description: t('subGraphModal.whenOutputNone.defaultDesc', { ns: 'workflow' }),
|
|
},
|
|
]), [t])
|
|
const selectedWhenOutputNoneOption = useMemo(() => (
|
|
whenOutputNoneOptions.find(item => item.value === nestedNodeConfig.null_strategy) ?? whenOutputNoneOptions[0]
|
|
), [nestedNodeConfig.null_strategy, whenOutputNoneOptions])
|
|
|
|
const handleNullStrategyChange = useCallback((value: NestedNodeConfig['null_strategy']) => {
|
|
onNestedNodeConfigChange({
|
|
...nestedNodeConfig,
|
|
null_strategy: value,
|
|
})
|
|
}, [nestedNodeConfig, onNestedNodeConfigChange])
|
|
|
|
const handleDefaultValueChange = useCallback((value: string) => {
|
|
const trimmed = value.trim()
|
|
let nextValue: unknown = value
|
|
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
try {
|
|
nextValue = JSON.parse(trimmed)
|
|
}
|
|
catch {
|
|
nextValue = value
|
|
}
|
|
}
|
|
|
|
onNestedNodeConfigChange({
|
|
...nestedNodeConfig,
|
|
default_value: nextValue,
|
|
})
|
|
}, [nestedNodeConfig, onNestedNodeConfigChange])
|
|
const defaultValue = nestedNodeConfig.default_value ?? ''
|
|
const shouldFormatDefaultValue = typeof defaultValue !== 'string'
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div className="px-4 pb-2 pt-4">
|
|
<div className="system-lg-semibold text-text-primary">
|
|
{t('subGraphModal.internalStructure', { ns: 'workflow' })}
|
|
</div>
|
|
<div className="system-sm-regular text-text-tertiary">
|
|
{t('subGraphModal.internalStructureDesc', { ns: 'workflow', name: agentName })}
|
|
</div>
|
|
</div>
|
|
<div className="px-4 pb-2">
|
|
<Tab value={tabType} onChange={setTabType} />
|
|
</div>
|
|
{tabType === TabType.lastRun && (
|
|
<div className="flex flex-1 items-center justify-center p-4">
|
|
<p className="system-sm-regular text-text-tertiary">
|
|
{t('subGraphModal.noRunHistory', { ns: 'workflow' })}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{tabType === TabType.settings && (
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="space-y-4 px-4 py-4">
|
|
<Field title={t('subGraphModal.outputVariables', { ns: 'workflow' })}>
|
|
<VarReferencePicker
|
|
nodeId={extractorNodeId}
|
|
readonly={false}
|
|
isShowNodeName
|
|
value={selectedOutput}
|
|
onChange={handleOutputVarChange}
|
|
availableNodes={availableNodes}
|
|
availableVars={availableVars}
|
|
/>
|
|
</Field>
|
|
</div>
|
|
<div className="space-y-4 px-4 py-4">
|
|
<Field
|
|
title={t('subGraphModal.whenOutputIsNone', { ns: 'workflow' })}
|
|
operations={(
|
|
<PortalToFollowElem
|
|
open={nullStrategyOpen}
|
|
onOpenChange={setNullStrategyOpen}
|
|
placement="bottom-end"
|
|
offset={4}
|
|
>
|
|
<PortalToFollowElemTrigger onClick={(e) => {
|
|
e.stopPropagation()
|
|
e.nativeEvent.stopImmediatePropagation()
|
|
setNullStrategyOpen(v => !v)
|
|
}}
|
|
>
|
|
<Button size="small">
|
|
{selectedWhenOutputNoneOption?.label}
|
|
<RiArrowDownSLine className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</PortalToFollowElemTrigger>
|
|
<PortalToFollowElemContent className="z-[70]">
|
|
<div className="w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
|
{whenOutputNoneOptions.map(option => (
|
|
<div
|
|
key={option.value}
|
|
className="flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
e.nativeEvent.stopImmediatePropagation()
|
|
handleNullStrategyChange(option.value)
|
|
setNullStrategyOpen(false)
|
|
}}
|
|
>
|
|
<div className="mr-1 w-4 shrink-0">
|
|
{nestedNodeConfig.null_strategy === option.value && (
|
|
<RiCheckLine className="h-4 w-4 text-text-accent" />
|
|
)}
|
|
</div>
|
|
<div className="grow">
|
|
<div className="system-sm-semibold mb-0.5 text-text-secondary">{option.label}</div>
|
|
<div className="system-xs-regular text-text-tertiary">{option.description}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</PortalToFollowElemContent>
|
|
</PortalToFollowElem>
|
|
)}
|
|
>
|
|
<div className="space-y-2">
|
|
{selectedWhenOutputNoneOption?.description && (
|
|
<div className="system-xs-regular text-text-tertiary">
|
|
{selectedWhenOutputNoneOption.description}
|
|
</div>
|
|
)}
|
|
{nestedNodeConfig.null_strategy === NULL_STRATEGY.USE_DEFAULT && (
|
|
<div className={cn('overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-normal p-1')}>
|
|
<CodeEditor
|
|
noWrapper
|
|
language={CodeLanguage.json}
|
|
value={defaultValue}
|
|
onChange={handleDefaultValueChange}
|
|
isJSONStringifyBeauty={shouldFormatDefaultValue}
|
|
className="min-h-[160px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default memo(ConfigPanel)
|