dify/web/app/components/workflow/panel/chat-variable-panel/index.tsx
2025-11-10 10:46:58 +08:00

298 lines
12 KiB
TypeScript

import {
memo,
useCallback,
useState,
} from 'react'
import { groupBy } from 'lodash-es'
import {
useStoreApi,
} from 'reactflow'
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { BubbleX, LongArrowLeft, LongArrowRight } from '@/app/components/base/icons/src/vender/line/others'
import BlockIcon from '@/app/components/workflow/block-icon'
import VariableModalTrigger from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger'
import VariableItem from '@/app/components/workflow/panel/chat-variable-panel/components/variable-item'
import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm'
import type {
ConversationVariable,
MemoryVariable,
} from '@/app/components/workflow/types'
import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { BlockEnum } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import cn from '@/utils/classnames'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { ChatVarType } from './type'
import { useMemoryVariable } from '@/app/components/workflow/hooks'
import VariableItemWithNode from './components/variable-item-with-node'
const ChatVariablePanel = () => {
const { t } = useTranslation()
const docLink = useDocLink()
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const appScopeMemoryVariables = useStore(s => s.memoryVariables.filter(v => v.scope === 'app'))
const nodeScopeMemoryVariables = useStore((s) => {
return groupBy(s.memoryVariables.filter(v => v.scope === 'node'), 'node_id')
})
const {
handleAddMemoryVariable,
handleUpdateMemoryVariable,
handleDeleteMemoryVariable,
} = useMemoryVariable()
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const updateChatVarList = useStore(s => s.setConversationVariables)
const setMemoryVariables = useStore(s => s.setMemoryVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
invalidateConversationVarValues,
} = useInspectVarsCrud()
const handleVarChanged = useCallback((updateMemoryVariables?: boolean) => {
doSyncWorkflowDraft(false, {
onSuccess() {
if (!updateMemoryVariables)
invalidateConversationVarValues()
},
})
}, [doSyncWorkflowDraft, invalidateConversationVarValues])
const [showTip, setShowTip] = useState(true)
const [showVariableModal, setShowVariableModal] = useState(false)
const [currentVar, setCurrentVar] = useState<ConversationVariable | MemoryVariable>()
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable | MemoryVariable>()
const getEffectedNodes = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const { getNodes } = store.getState()
const allNodes = getNodes()
let valueSelector = []
if (chatVar.value_type === ChatVarType.Memory) {
const { node_id, id } = chatVar as MemoryVariable
valueSelector = [
'memory_block',
node_id ? `${node_id}_${id}` : id,
]
}
else {
valueSelector = ['conversation', chatVar.name]
}
return findUsedVarNodes(
valueSelector,
allNodes,
)
}, [store])
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const { getNodes, setNodes } = store.getState()
const effectedNodes = getEffectedNodes(chatVar)
const newNodes = getNodes().map((node) => {
if (effectedNodes.find(n => n.id === node.id))
return updateNodeVars(node, ['conversation', chatVar.name], [])
return node
})
setNodes(newNodes)
}, [getEffectedNodes, store])
const handleEdit = (chatVar: ConversationVariable | MemoryVariable) => {
setCurrentVar(chatVar)
setShowVariableModal(true)
}
const handleDelete = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
removeUsedVarInNodes(chatVar)
const varList = workflowStore.getState().conversationVariables
updateChatVarList(varList.filter(v => v.id !== chatVar.id))
if (chatVar.value_type === ChatVarType.Memory)
handleDeleteMemoryVariable(chatVar as MemoryVariable)
setCacheForDelete(undefined)
setShowRemoveConfirm(false)
handleVarChanged(chatVar.value_type === ChatVarType.Memory)
}, [handleVarChanged, removeUsedVarInNodes, updateChatVarList, setMemoryVariables])
const deleteCheck = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const effectedNodes = getEffectedNodes(chatVar)
if (effectedNodes.length > 0) {
setCacheForDelete(chatVar)
setShowRemoveConfirm(true)
}
else {
handleDelete(chatVar)
}
}, [getEffectedNodes, handleDelete])
const handleSave = useCallback(async (chatVar: ConversationVariable | MemoryVariable) => {
if (chatVar.value_type === ChatVarType.Memory) {
if (!currentVar)
handleAddMemoryVariable(chatVar as MemoryVariable)
else
handleUpdateMemoryVariable(chatVar as MemoryVariable)
handleVarChanged(true)
return
}
const varList = workflowStore.getState().conversationVariables
// add chatVar
if (!currentVar) {
const newList = [chatVar, ...varList] as ConversationVariable[]
updateChatVarList(newList)
handleVarChanged()
return
}
// edit chatVar
const newList = varList.map(v => v.id === currentVar.id ? chatVar : v)
updateChatVarList(newList as ConversationVariable[])
// side effects of rename env
if (currentVar.name !== chatVar.name) {
const { getNodes, setNodes } = store.getState()
const effectedNodes = getEffectedNodes(currentVar)
const newNodes = getNodes().map((node) => {
if (effectedNodes.find(n => n.id === node.id))
return updateNodeVars(node, ['conversation', currentVar.name], ['conversation', chatVar.name])
return node
})
setNodes(newNodes)
}
handleVarChanged()
}, [currentVar, getEffectedNodes, handleVarChanged, store, updateChatVarList])
return (
<div
className={cn(
'relative flex h-full w-[420px] flex-col rounded-l-2xl border border-components-panel-border bg-components-panel-bg-alt',
)}
>
<div className='system-xl-semibold flex shrink-0 items-center justify-between p-4 pb-0 text-text-primary'>
{t('workflow.chatVariable.panelTitle')}
<div className='flex items-center gap-1'>
<ActionButton state={showTip ? ActionButtonState.Active : undefined} onClick={() => setShowTip(!showTip)}>
<RiBookOpenLine className='h-4 w-4' />
</ActionButton>
<div
className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={() => setShowChatVariablePanel(false)}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
{showTip && (
<div className='shrink-0 px-3 pb-2 pt-2.5'>
<div className='radius-2xl relative bg-background-section-burn p-3'>
<div className='system-2xs-medium-uppercase inline-block rounded-[5px] border border-divider-deep px-[5px] py-[3px] text-text-tertiary'>TIPS</div>
<div className='system-sm-regular mb-4 mt-1 text-text-secondary'>
{t('workflow.chatVariable.panelDescription')}
<a target='_blank' rel='noopener noreferrer' className='text-text-accent'
href={docLink('/guides/workflow/variables#conversation-variables', {
'zh-Hans': '/guides/workflow/variables#会话变量',
'ja-JP': '/guides/workflow/variables#会話変数',
})}>
{t('workflow.chatVariable.docLink')}
</a>
</div>
<div className='flex items-center gap-2'>
<div className='radius-lg flex flex-col border border-workflow-block-border bg-workflow-block-bg p-3 pb-4 shadow-md'>
<BubbleX className='mb-1 h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
<div className='system-xs-semibold text-text-secondary'>conversation_var</div>
<div className='system-2xs-regular text-text-tertiary'>String</div>
</div>
<div className='grow'>
<div className='mb-2 flex items-center gap-2 py-1'>
<div className='flex h-3 w-16 shrink-0 items-center gap-1 px-1'>
<LongArrowLeft className='h-2 grow text-text-quaternary' />
<div className='system-2xs-medium shrink-0 text-text-tertiary'>WRITE</div>
</div>
<BlockIcon className='shrink-0' type={BlockEnum.Assigner} />
<div className='system-xs-semibold grow truncate text-text-secondary'>{t('workflow.blocks.assigner')}</div>
</div>
<div className='flex items-center gap-2 py-1'>
<div className='flex h-3 w-16 shrink-0 items-center gap-1 px-1'>
<div className='system-2xs-medium shrink-0 text-text-tertiary'>READ</div>
<LongArrowRight className='h-2 grow text-text-quaternary' />
</div>
<BlockIcon className='shrink-0' type={BlockEnum.LLM} />
<div className='system-xs-semibold grow truncate text-text-secondary'>{t('workflow.blocks.llm')}</div>
</div>
</div>
</div>
<div className='absolute right-[38px] top-[-4px] z-10 h-3 w-3 rotate-45 bg-background-section-burn' />
</div>
</div>
)}
<div className='shrink-0 px-4 pb-3 pt-2'>
<VariableModalTrigger
open={showVariableModal}
setOpen={setShowVariableModal}
showTip={showTip}
chatVar={currentVar}
onSave={handleSave}
onClose={() => setCurrentVar(undefined)}
/>
</div>
<div className='grow overflow-y-auto rounded-b-2xl px-4'>
{
appScopeMemoryVariables.map(memoryVariable => (
<VariableItem
key={memoryVariable.id}
item={memoryVariable}
onEdit={handleEdit}
onDelete={deleteCheck}
term={memoryVariable.term}
scope='app'
currentVarId={currentVar?.id}
/>
))
}
{varList.map(chatVar => (
<VariableItem
key={chatVar.id}
item={chatVar}
onEdit={handleEdit}
onDelete={deleteCheck}
currentVarId={currentVar?.id}
/>
))}
{
!!Object.keys(nodeScopeMemoryVariables).length && (
<div className='pb-1 pt-4'>
<div className='system-xs-medium-uppercase mb-1 flex items-center space-x-2 text-text-tertiary'>
{t('workflow.chatVariable.nodeScopeMemory')}
<div className='ml-2 h-px grow bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent'></div>
</div>
{
Object.keys(nodeScopeMemoryVariables).map(nodeId => (
<VariableItemWithNode
key={nodeId}
nodeId={nodeId}
memoryVariables={nodeScopeMemoryVariables[nodeId]}
onEdit={handleEdit}
onDelete={deleteCheck}
currentVarId={currentVar?.id}
/>
))
}
</div>
)
}
</div>
<RemoveEffectVarConfirm
isShow={showRemoveVarConfirm}
onCancel={() => setShowRemoveConfirm(false)}
onConfirm={() => cacheForDelete && handleDelete(cacheForDelete)}
/>
</div>
)
}
export default memo(ChatVariablePanel)