support create memory var in memory popup

This commit is contained in:
JzoNg 2025-09-23 17:42:53 +08:00
parent 05c05bb6d0
commit efcaa2bbbd
5 changed files with 222 additions and 180 deletions

View File

@ -25,7 +25,7 @@ import {
} from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/add-memory-button'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
import Divider from '@/app/components/base/divider'
import VariableIcon from '@/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon'
import type {
@ -135,15 +135,25 @@ export default function MemoryPopupPlugin({
setOpen(false)
}, [setOpen])
const handleSelectVariable = useCallback((variable: string[]) => {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variable)
closePortal()
}, [editor, closePortal])
const handleCreate = useCallback(() => {
eventEmitter?.emit({ type: MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER, instanceId } as any)
closePortal()
}, [eventEmitter, instanceId, closePortal])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
openPortal()
})
const handleSelectVariable = useCallback((variable: string[]) => {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variable)
closePortal()
}, [editor, closePortal])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER && v.instanceId === instanceId)
handleSelectVariable(v.variable)
})
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
@ -245,7 +255,7 @@ export default function MemoryPopupPlugin({
</div>
</div>
)}
<div className='system-xs-medium flex items-center gap-1 border-t border-divider-subtle px-4 py-2 text-text-accent-light-mode-only'>
<div className='system-xs-medium flex cursor-pointer items-center gap-1 border-t border-divider-subtle px-4 py-2 text-text-accent-light-mode-only' onClick={handleCreate}>
<RiAddLine className='h-4 w-4' />
<div>{t('workflow.nodes.llm.memory.createButton')}</div>
</div>

View File

@ -2,9 +2,6 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
export const MEMORY_POPUP_SHOW_BY_EVENT_EMITTER = 'MEMORY_POPUP_SHOW_BY_EVENT_EMITTER'
export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
type Props = {
onAddMemory: () => void
}

View File

@ -36,10 +36,12 @@ import Switch from '@/app/components/base/switch'
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
import { useStore } from '@/app/components/workflow/store'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import AddMemoryButton, { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from './add-memory-button'
import AddMemoryButton from './add-memory-button'
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from './type'
import type {
ConversationVariable,
} from '@/app/components/workflow/types'
import MemoryCreateButton from '@/app/components/workflow/nodes/llm/components/memory-system/memory-create-button'
type Props = {
className?: string
@ -169,175 +171,178 @@ const Editor: FC<Props> = ({
}
return (
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? (gradientBorder && 'bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2') : 'bg-transparent', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
<PromptGeneratorBtn
nodeId={nodeId!}
editorId={editorId}
className='ml-[5px]'
onGenerated={onGenerated}
modelConfig={modelConfig}
currentPrompt={value}
/>
)}
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
{/* Operations */}
<div className='flex items-center space-x-[2px]'>
{isSupportJinja && (
<Tooltip
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
>
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Jinja className='h-3 w-6 text-text-quaternary' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
</Tooltip>
)}
{!readOnly && (
<Tooltip
popupContent={`${t('workflow.common.insertVarTip')}`}
>
<ActionButton onClick={handleInsertVariable}>
<Variable02 className='h-4 w-4' />
</ActionButton>
</Tooltip>
)}
{showRemove && (
<ActionButton onClick={onRemove}>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
)}
{!isCopied
? (
<ActionButton onClick={handleCopy}>
<Copy className='h-4 w-4' />
</ActionButton>
)
: (
<ActionButton>
<CopyCheck className='h-4 w-4' />
</ActionButton>
)
}
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
<>
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? (gradientBorder && 'bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2') : 'bg-transparent', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
</div>
</div>
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex grow flex-col', isMemorySupported && isFocus && 'pb-1.5')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
isMemorySupported
memoryVarInNode={memoryVarInNode}
memoryVarInApp={memoryVarInApp}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{isMemorySupported && <AddMemoryButton onAddMemory={handleAddMemory} />}
</>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
<div className='flex items-center'>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
<PromptGeneratorBtn
nodeId={nodeId!}
editorId={editorId}
className='ml-[5px]'
onGenerated={onGenerated}
modelConfig={modelConfig}
currentPrompt={value}
/>
)}
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
{/* Operations */}
<div className='flex items-center space-x-[2px]'>
{isSupportJinja && (
<Tooltip
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
>
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Jinja className='h-3 w-6 text-text-quaternary' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
</Tooltip>
)}
{!readOnly && (
<Tooltip
popupContent={`${t('workflow.common.insertVarTip')}`}
>
<ActionButton onClick={handleInsertVariable}>
<Variable02 className='h-4 w-4' />
</ActionButton>
</Tooltip>
)}
{showRemove && (
<ActionButton onClick={onRemove}>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
)}
{!isCopied
? (
<ActionButton onClick={handleCopy}>
<Copy className='h-4 w-4' />
</ActionButton>
)
: (
<ActionButton>
<CopyCheck className='h-4 w-4' />
</ActionButton>
)
}
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
</div>
)}
</div>
</div>
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex grow flex-col', isMemorySupported && isFocus && 'pb-1.5')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
isMemorySupported
memoryVarInNode={memoryVarInNode}
memoryVarInApp={memoryVarInApp}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{isMemorySupported && <AddMemoryButton onAddMemory={handleAddMemory} />}
</>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
)}
</div>
</div>
</div>
</div>
</Wrap>
</Wrap>
{isMemorySupported && <MemoryCreateButton instanceId={instanceId} hideTrigger />}
</>
)
}

View File

@ -0,0 +1,3 @@
export const MEMORY_POPUP_SHOW_BY_EVENT_EMITTER = 'MEMORY_POPUP_SHOW_BY_EVENT_EMITTER'
export const MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER = 'MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER'
export const MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER = 'MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER'

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'
import { RiAddLine } from '@remixicon/react'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
import type { OffsetOptions, Placement } from '@floating-ui/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@ -11,8 +12,23 @@ import type { ConversationVariable } from '@/app/components/workflow/types'
import { useStore } from '@/app/components/workflow/store'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
const MemoryCreateButton = () => {
type Props = {
placement?: Placement
offset?: number | OffsetOptions
hideTrigger?: boolean
instanceId?: string
}
const MemoryCreateButton = ({
placement,
offset,
hideTrigger,
instanceId,
}: Props) => {
const { eventEmitter } = useEventEmitterContextContext()
const [open, setOpen] = useState(false)
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const updateChatVarList = useStore(s => s.setConversationVariables)
@ -34,19 +50,30 @@ const MemoryCreateButton = () => {
updateChatVarList(newList)
handleVarChanged()
setOpen(false)
}, [varList, updateChatVarList, handleVarChanged, setOpen])
if (instanceId)
eventEmitter?.emit({ type: MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, instanceId, variable: ['conversation', newChatVar.name] } as any)
}, [varList, updateChatVarList, handleVarChanged, setOpen, eventEmitter, instanceId])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
setOpen(true)
})
return (
<>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='left'
placement={placement || 'left'}
offset={offset}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<ActionButton className='shrink-0'>
<RiAddLine className='h-4 w-4' />
</ActionButton>
{hideTrigger && <div></div>}
{!hideTrigger && (
<ActionButton className='shrink-0'>
<RiAddLine className='h-4 w-4' />
</ActionButton>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<VariableModal