This commit is contained in:
zxhlyh 2025-11-10 10:46:58 +08:00
parent 2f0076166a
commit 4050ed8c8e
31 changed files with 361 additions and 153 deletions

View File

@ -53,6 +53,7 @@ export type IGetAutomaticResProps = {
editorId?: string
currentPrompt?: string
isBasicMode?: boolean
hideTryIt?: boolean
}
const TryLabel: FC<{
@ -81,6 +82,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
currentPrompt,
isBasicMode,
onFinished,
hideTryIt,
}) => {
const { t } = useTranslation()
const localModel = localStorage.getItem('auto-gen-model')
@ -307,7 +309,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
hideDebugWithMultipleModel
/>
</div>
{isBasicMode && (
{isBasicMode && !hideTryIt && (
<div className='mt-4'>
<div className='flex items-center'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>

View File

@ -22,7 +22,7 @@ import Radio from '@/app/components/base/radio'
import RadioE from '@/app/components/base/radio/ui'
import Textarea from '@/app/components/base/textarea'
import PromptEditor from '@/app/components/base/prompt-editor'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list'
import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list'
import ArrayBooleanValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'
@ -136,6 +136,7 @@ const BaseField = ({
const handleChange = useCallback((value: any) => {
if (disabled)
return
field.handleChange(value)
formOnChange?.(field.form, value)
onChange?.(field.name, value)
@ -340,10 +341,12 @@ const BaseField = ({
selfProps?.enablePromptGenerator && (
<PromptGeneratorBtn
nodeId={selfProps?.nodeId}
editorId={selfProps?.editorId}
className='absolute right-0 top-[-26px]'
onGenerated={handleChange}
modelConfig={selfProps?.modelConfig}
currentPrompt={value}
isBasicMode={selfProps?.isBasicMode}
/>
)
}
@ -405,11 +408,28 @@ const BaseField = ({
type === FormTypeEnum.modelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
value={value}
setModel={handleChange}
mode={value?.mode}
modelId={value?.name}
provider={value?.provider}
setModel={({ modelId, mode, provider }) => {
handleChange({
mode,
provider,
name: modelId,
completion_params: value?.completion_params,
})
}}
completionParams={value?.completion_params}
onCompletionParamsChange={(params) => {
handleChange({
...value,
completion_params: params,
})
}}
readonly={disabled}
scope={formSchema.scope}
isAdvancedMode
isInWorkflow
hideDebugWithMultipleModel
/>
)
}

View File

@ -10,6 +10,7 @@ export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
export const UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER = 'prompt-editor-workflow-variables-block-update-variables'
export const checkHasContextBlock = (text: string) => {
if (!text)

View File

@ -78,9 +78,11 @@ import type {
import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import cn from '@/utils/classnames'
import PromptEditorProvider from './store/provider'
export type PromptEditorProps = {
instanceId?: string
@ -176,6 +178,13 @@ const PromptEditor: FC<PromptEditorProps> = ({
payload: historyBlock?.history,
} as any)
}, [eventEmitter, historyBlock?.history])
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER,
payload: workflowVariableBlock?.variables,
instanceId,
} as any)
}, [eventEmitter, workflowVariableBlock?.variables])
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
@ -267,7 +276,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
{
workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock } />
<WorkflowVariableBlock {...workflowVariableBlock} />
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
</>
)
@ -311,4 +320,12 @@ const PromptEditor: FC<PromptEditorProps> = ({
)
}
export default PromptEditor
const PromptEditorWithProvider = ({ instanceId, ...props }: PromptEditorProps) => {
return (
<PromptEditorProvider instanceId={instanceId}>
<PromptEditor {...props} instanceId={instanceId} />
</PromptEditorProvider>
)
}
export default PromptEditorWithProvider

View File

@ -24,23 +24,26 @@ import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type {
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import {
VariableLabelInEditor,
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER } from '../../constants'
import { usePromptEditorStore } from '../../store/store'
type WorkflowVariableBlockComponentProps = {
nodeKey: string
variables: string[]
workflowNodesMap: WorkflowNodesMap
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
availableVariables: NodeOutPutVar[]
getVarType?: (payload: {
nodeId: string,
valueSelector: ValueSelector,
}) => Type
isMemorySupported?: boolean
}
const WorkflowVariableBlockComponent = ({
@ -48,13 +51,27 @@ const WorkflowVariableBlockComponent = ({
variables,
workflowNodesMap = {},
getVarType,
environmentVariables,
conversationVariables,
ragVariables,
isMemorySupported,
availableVariables: initialAvailableVariables,
}: WorkflowVariableBlockComponentProps) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const instanceId = usePromptEditorStore(s => s.instanceId)
const { eventEmitter } = useEventEmitterContextContext()
const [availableVariables, setAvailableVariables] = useState<NodeOutPutVar[]>(initialAvailableVariables)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_WORKFLOW_VARIABLES_EVENT_EMITTER && instanceId && v.instanceId === instanceId)
setAvailableVariables(v.payload)
})
const environmentVariables = availableVariables?.find(v => v.nodeId === 'env')?.vars || []
const conversationVariables = availableVariables?.find(v => v.nodeId === 'conversation')?.vars || []
const memoryVariables = conversationVariables?.filter(v => v.variable.startsWith('memory_block.'))
const ragVariables = availableVariables?.reduce<any[]>((acc, curr) => {
if (curr.nodeId === 'rag')
acc.push(...curr.vars)
else
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, [])
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
const variablesLength = variables.length
const isRagVar = isRagVariableVar(variables)
@ -73,27 +90,19 @@ const WorkflowVariableBlockComponent = ({
const isMemoryVar = isMemoryVariable(variables)
const isException = isExceptionVariable(varName, node?.type)
const memoryVariables = conversationVariables?.filter(v => v.variable.startsWith('memory_block.'))
let variableValid = true
if (isEnv) {
if (environmentVariables)
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
variableValid = environmentVariables.some(v =>
v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isConversationVar) {
if (conversationVariables)
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isMemoryVar) {
if (memoryVariables)
variableValid = memoryVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
if (!isMemorySupported)
variableValid = false
variableValid = memoryVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
}
else if (isRagVar) {
if (ragVariables)
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
}
else {
variableValid = !!node

View File

@ -33,18 +33,9 @@ const WorkflowVariableBlock = memo(({
onDelete,
getVarType,
variables: originalVariables,
isMemorySupported,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
const ragVariables = originalVariables?.reduce<any[]>((acc, curr) => {
if (curr.nodeId === 'rag')
acc.push(...curr.vars)
else
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, [])
useEffect(() => {
editor.update(() => {
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
@ -60,7 +51,7 @@ const WorkflowVariableBlock = memo(({
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
(variables: string[]) => {
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, originalVariables?.find(o => o.nodeId === 'env')?.vars || [], originalVariables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables, isMemorySupported)
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, originalVariables || [])
$insertNodes([workflowVariableBlockNode])
if (onInsert)

View File

@ -3,7 +3,7 @@ import { DecoratorNode } from 'lexical'
import type { WorkflowVariableBlockType } from '../../types'
import WorkflowVariableBlockComponent from './component'
import type { GetVarType } from '../../types'
import type { Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
@ -11,27 +11,21 @@ export type SerializedNode = SerializedLexicalNode & {
variables: string[]
workflowNodesMap: WorkflowNodesMap
getVarType?: GetVarType
environmentVariables?: Var[]
conversationVariables?: Var[]
ragVariables?: Var[]
isMemorySupported?: boolean
availableVariables?: NodeOutPutVar[]
}
export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> {
__variables: string[]
__workflowNodesMap: WorkflowNodesMap
__getVarType?: GetVarType
__environmentVariables?: Var[]
__conversationVariables?: Var[]
__ragVariables?: Var[]
__isMemorySupported?: boolean
__availableVariables?: NodeOutPutVar[]
static getType(): string {
return 'workflow-variable-block'
}
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables)
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__availableVariables)
}
isInline(): boolean {
@ -43,20 +37,14 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
workflowNodesMap: WorkflowNodesMap,
getVarType: any,
key?: NodeKey,
environmentVariables?: Var[],
conversationVariables?: Var[],
ragVariables?: Var[],
isMemorySupported?: boolean,
availableVariables?: NodeOutPutVar[],
) {
super(key)
this.__variables = variables
this.__workflowNodesMap = workflowNodesMap
this.__getVarType = getVarType
this.__environmentVariables = environmentVariables
this.__conversationVariables = conversationVariables
this.__ragVariables = ragVariables
this.__isMemorySupported = isMemorySupported
this.__availableVariables = availableVariables
}
createDOM(): HTMLElement {
@ -76,16 +64,13 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
variables={this.__variables}
workflowNodesMap={this.__workflowNodesMap}
getVarType={this.__getVarType!}
environmentVariables={this.__environmentVariables}
conversationVariables={this.__conversationVariables}
ragVariables={this.__ragVariables}
isMemorySupported={this.__isMemorySupported}
availableVariables={this.__availableVariables || []}
/>
)
}
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables, serializedNode.isMemorySupported)
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.availableVariables)
return node
}
@ -97,9 +82,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
variables: this.getVariables(),
workflowNodesMap: this.getWorkflowNodesMap(),
getVarType: this.getVarType(),
environmentVariables: this.getEnvironmentVariables(),
conversationVariables: this.getConversationVariables(),
ragVariables: this.getRagVariables(),
availableVariables: this.getAvailableVariables(),
}
}
@ -118,32 +101,17 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element>
return self.__getVarType
}
getEnvironmentVariables(): any {
getAvailableVariables(): NodeOutPutVar[] {
const self = this.getLatest()
return self.__environmentVariables
}
getConversationVariables(): any {
const self = this.getLatest()
return self.__conversationVariables
}
getRagVariables(): any {
const self = this.getLatest()
return self.__ragVariables
}
getIsMemorySupported() {
const self = this.getLatest()
return self.__isMemorySupported
return self.__availableVariables || []
}
getTextContent(): string {
return `{{#${this.getVariables().join('.')}#}}`
}
}
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[], isMemorySupported?: boolean): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables, isMemorySupported)
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, availableVariables?: NodeOutPutVar[]): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, availableVariables)
}
export function $isWorkflowVariableBlockNode(

View File

@ -19,16 +19,8 @@ const WorkflowVariableBlockReplacementBlock = ({
getVarType,
onInsert,
variables,
isMemorySupported,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
const ragVariables = variables?.reduce<any[]>((acc, curr) => {
if (curr.nodeId === 'rag')
acc.push(...curr.vars)
else
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, [])
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@ -40,7 +32,7 @@ const WorkflowVariableBlockReplacementBlock = ({
onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables, isMemorySupported))
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables))
}, [onInsert, workflowNodesMap, getVarType, variables])
const getMatch = useCallback((text: string) => {

View File

@ -0,0 +1,31 @@
import { createContext, useRef } from 'react'
import { createPromptEditorStore } from './store'
type PromptEditorStoreApi = ReturnType<typeof createPromptEditorStore>
type PromptEditorContextType = PromptEditorStoreApi | undefined
export const PromptEditorContext = createContext<PromptEditorContextType>(undefined)
type PromptEditorProviderProps = {
instanceId?: string
children: React.ReactNode
}
const PromptEditorProvider = ({
instanceId,
children,
}: PromptEditorProviderProps) => {
const storeRef = useRef<PromptEditorStoreApi>(undefined)
if (!storeRef.current)
storeRef.current = createPromptEditorStore({ instanceId })
return (
<PromptEditorContext.Provider value={storeRef.current!}>
{children}
</PromptEditorContext.Provider>
)
}
export default PromptEditorProvider

View File

@ -0,0 +1,24 @@
import { useContext } from 'react'
import { createStore, useStore } from 'zustand'
import { PromptEditorContext } from './provider'
type PromptEditorStoreProps = {
instanceId?: string
}
type PromptEditorStore = {
instanceId?: string
}
export const createPromptEditorStore = ({ instanceId }: PromptEditorStoreProps) => {
return createStore<PromptEditorStore>(() => ({
instanceId,
}))
}
export const usePromptEditorStore = <T>(selector: (state: PromptEditorStore) => T): T => {
const store = useContext(PromptEditorContext)
if (!store)
throw new Error('Missing PromptEditorContext.Provider in the tree')
return useStore(store, selector)
}

View File

@ -129,8 +129,8 @@ export const useProviderCredentialsAndLoadBalancing = (
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
}
export const useModelList = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${type}`, fetchModelList)
export const useModelList = (type: ModelTypeEnum, enabled?: boolean) => {
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/models/model-types/${type}` : null, fetchModelList)
return {
data: data?.data || [],
@ -139,8 +139,8 @@ export const useModelList = (type: ModelTypeEnum) => {
}
}
export const useDefaultModel = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${type}`, fetchDefaultModal)
export const useDefaultModel = (type: ModelTypeEnum, enabled?: boolean) => {
const { data, mutate, isLoading } = useSWR(enabled ? `/workspaces/current/default-model?model_type=${type}` : null, fetchDefaultModal)
return {
data: data?.data,

View File

@ -85,17 +85,11 @@ export const useNodesSyncDraft = () => {
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
memory_blocks: memoryVariables.map(({ value_type, scope, more, node_id, model, ...rest }) => {
memory_blocks: memoryVariables.map(({ value_type, scope, more, node_id, ...rest }) => {
return {
...rest,
node_id: scope === 'node' ? node_id : undefined,
scope,
model: model ? {
completion_params: model.completion_params,
mode: model.mode,
provider: model.provider,
name: model.model,
} : undefined,
}
}),
hash: syncWorkflowDraftHash,

View File

@ -40,12 +40,6 @@ export const useFormatMemoryVariables = () => {
return {
...v,
value_type: ChatVarType.Memory,
model: v.model ? {
completion_params: v.model?.completion_params,
mode: v.model?.mode,
provider: v.model?.provider,
model: v.model?.name,
} : undefined,
}
})
}, [])

View File

@ -823,7 +823,7 @@ export const toNodeOutputVars = (
let nodeList = []
if (conversationVariablesFirst) {
nodeList = [
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
...((isChatMode && (conversationVariables.length > 0 || memoryVariables.length > 0)) ? [CHAT_VAR_NODE] : []),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
@ -835,7 +835,7 @@ export const toNodeOutputVars = (
nodeList = [
...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
...((isChatMode && (conversationVariables.length > 0 || memoryVariables.length > 0)) ? [CHAT_VAR_NODE] : []),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
? [RAG_PIPELINE_NODE]
: []),

View File

@ -16,6 +16,7 @@ type Params = {
filterVar: (payload: Var, selector: ValueSelector) => boolean
passedInAvailableNodes?: Node[]
conversationVariablesFirst?: boolean
isMemorySupported?: boolean
}
// TODO: loop type?
@ -26,6 +27,7 @@ const useAvailableVarList = (nodeId: string, {
hideChatVar,
passedInAvailableNodes,
conversationVariablesFirst,
isMemorySupported,
}: Params = {
onlyLeafNodeVar: false,
filterVar: () => true,
@ -80,6 +82,11 @@ const useAvailableVarList = (nodeId: string, {
return {
...availableVar,
vars: availableVar.vars.filter((v) => {
if (!v.isMemoryVariable)
return true
if (!isMemorySupported)
return false
if (!v.memoryVariableNodeId)
return true

View File

@ -76,6 +76,7 @@ const ConfigPrompt: FC<Props> = ({
onlyLeafNodeVar: false,
filterVar,
conversationVariablesFirst: true,
isMemorySupported,
})
const handleChatModePromptChange = useCallback((index: number) => {

View File

@ -28,20 +28,11 @@ const BlockMemory = ({ id }: BlockMemoryProps) => {
editMemoryVariable,
handleSetEditMemoryVariable,
handleEdit,
handleDelete,
handleDeleteConfirm,
cacheForDeleteMemoryVariable,
setCacheForDeleteMemoryVariable,
} = useMemoryVariables(id)
const [showConfirm, setShowConfirm] = useState<{
title: string
desc: string
onConfirm: () => void
} | undefined>(undefined)
const handleDelete = (blockId: string) => {
setShowConfirm({
title: t('workflow.nodes.common.memory.block.deleteConfirmTitle'),
desc: t('workflow.nodes.common.memory.block.deleteConfirmDesc'),
onConfirm: () => handleDelete(blockId),
})
}
if (!memoryVariablesInUsed?.length) {
return (
@ -87,7 +78,7 @@ const BlockMemory = ({ id }: BlockMemoryProps) => {
editMemoryVariable?.id === memoryVariable.id && 'inline-flex',
)}
size='m'
onClick={() => handleDelete(memoryVariable.id)}
onClick={() => handleDelete(memoryVariable)}
onMouseOver={() => setDestructiveItemId(memoryVariable.id)}
onMouseOut={() => setDestructiveItemId(undefined)}
>
@ -98,13 +89,13 @@ const BlockMemory = ({ id }: BlockMemoryProps) => {
}
</div>
{
!!showConfirm && (
!!cacheForDeleteMemoryVariable && (
<Confirm
isShow
onCancel={() => setShowConfirm(undefined)}
onConfirm={showConfirm.onConfirm}
title={showConfirm.title}
content={showConfirm.desc}
onCancel={() => setCacheForDeleteMemoryVariable(undefined)}
onConfirm={() => handleDeleteConfirm(cacheForDeleteMemoryVariable.id)}
title={t('workflow.nodes.common.memory.deleteNodeMemoryVariableConfirmTitle', { name: cacheForDeleteMemoryVariable.name })}
content={t('workflow.nodes.common.memory.deleteNodeMemoryVariableConfirmDesc')}
/>
)
}

View File

@ -0,0 +1,20 @@
import { useCallback } from 'react'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useNodeUpdate } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { findUsedVarNodes } from '@/app/components/workflow/nodes/_base/components/variable/utils'
export const useMemoryUsedDetector = (nodeId: string) => {
const { getNodeData } = useNodeUpdate(nodeId)
const getMemoryUsedDetector = useCallback((chatVar: MemoryVariable) => {
const nodeData = getNodeData()!
const valueSelector = ['memory_block', chatVar.node_id ? `${chatVar.node_id}_${chatVar.id}` : chatVar.id]
return findUsedVarNodes(
valueSelector,
[nodeData],
)
}, [getNodeData])
return {
getMemoryUsedDetector,
}
}

View File

@ -9,21 +9,20 @@ import {
} from '@/app/components/workflow/store'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { useMemoryUsedDetector } from './use-memory-used-detector'
export const useMemoryVariables = (nodeId: string) => {
const workflowStore = useWorkflowStore()
const memoryVariables = useStore(s => s.memoryVariables)
const [editMemoryVariable, setEditMemoryVariable] = useState<MemoryVariable | undefined>(undefined)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getMemoryUsedDetector } = useMemoryUsedDetector(nodeId)
const [cacheForDeleteMemoryVariable, setCacheForDeleteMemoryVariable] = useState<MemoryVariable | undefined>(undefined)
const memoryVariablesInUsed = useMemo(() => {
return memoryVariables.filter(variable => variable.node_id === nodeId)
}, [memoryVariables, nodeId])
const handleDelete = (blockId: string) => {
console.log('delete', blockId)
}
const handleSave = useCallback((newMemoryVar: MemoryVariable) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = [newMemoryVar, ...memoryVariables]
@ -47,6 +46,23 @@ export const useMemoryVariables = (nodeId: string) => {
handleSyncWorkflowDraft()
}
const handleDeleteConfirm = (memoryVariableId?: string) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = memoryVariables.filter(variable => variable.id !== memoryVariableId)
setMemoryVariables(newList)
handleSyncWorkflowDraft()
setCacheForDeleteMemoryVariable(undefined)
}
const handleDelete = (memoryVariable: MemoryVariable) => {
const effectedNodes = getMemoryUsedDetector(memoryVariable)
if (effectedNodes.length > 0) {
setCacheForDeleteMemoryVariable(memoryVariable)
return
}
handleDeleteConfirm(memoryVariable.id)
}
return {
memoryVariablesInUsed,
handleDelete,
@ -54,5 +70,8 @@ export const useMemoryVariables = (nodeId: string) => {
handleSetEditMemoryVariable,
handleEdit,
editMemoryVariable,
handleDeleteConfirm,
cacheForDeleteMemoryVariable,
setCacheForDeleteMemoryVariable,
}
}

View File

@ -10,11 +10,14 @@ import {
} from '../linear-memory'
import type { Memory } from '@/app/components/workflow/types'
import { MemoryMode } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useMemoryUsedDetector } from './use-memory-used-detector'
export const useMemory = (
id: string,
data: LLMNodeType,
) => {
const workflowStore = useWorkflowStore()
const { memory } = data
const initCollapsed = useMemo(() => {
if (!memory?.enabled)
@ -27,10 +30,12 @@ export const useMemory = (
getNodeData,
handleNodeDataUpdate,
} = useNodeUpdate(id)
const [showTipsWhenMemoryModeBlockToLinear, setShowTipsWhenMemoryModeBlockToLinear] = useState(false)
const { getMemoryUsedDetector } = useMemoryUsedDetector(id)
const handleMemoryTypeChange = useCallback((value: string) => {
const nodeData = getNodeData()
const { memory: memoryData = {} } = nodeData as any
const { memory: memoryData = {} as Memory } = nodeData?.data as LLMNodeType
if (value === MemoryMode.disabled) {
setCollapsed(true)
@ -66,8 +71,34 @@ export const useMemory = (
},
})
}
setShowTipsWhenMemoryModeBlockToLinear(false)
}, [getNodeData, handleNodeDataUpdate])
const handleMemoryTypeChangeBefore = useCallback((value: string) => {
const nodeData = getNodeData()
const { memory: memoryData = {} as Memory } = nodeData?.data as LLMNodeType
const { memoryVariables } = workflowStore.getState()
if (memoryData.mode === MemoryMode.block && value === MemoryMode.linear && nodeData) {
const globalMemoryVariables = memoryVariables.filter(variable => variable.scope === 'app')
const currentNodeMemoryVariables = memoryVariables.filter(variable => variable.node_id === id)
const allMemoryVariables = [...globalMemoryVariables, ...currentNodeMemoryVariables]
for (const variable of allMemoryVariables) {
const effectedNodes = getMemoryUsedDetector(variable)
if (effectedNodes.length > 0) {
setShowTipsWhenMemoryModeBlockToLinear(true)
return
}
}
handleMemoryTypeChange(value)
}
else {
handleMemoryTypeChange(value)
}
}, [getNodeData, workflowStore, handleMemoryTypeChange])
const handleUpdateMemory = useCallback((memory: Memory) => {
handleNodeDataUpdate({
memory,
@ -96,7 +127,10 @@ export const useMemory = (
collapsed,
setCollapsed,
handleMemoryTypeChange,
handleMemoryTypeChangeBefore,
memoryType,
handleUpdateMemory,
showTipsWhenMemoryModeBlockToLinear,
setShowTipsWhenMemoryModeBlockToLinear,
}
}

View File

@ -21,6 +21,7 @@ import { MemoryMode } from '@/app/components/workflow/types'
import BlockMemory from './block-memory'
import { ActionButton } from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
type MemoryProps = Pick<Node, 'id' | 'data'> & {
readonly?: boolean
@ -38,8 +39,11 @@ const MemorySystem = ({
collapsed,
setCollapsed,
handleMemoryTypeChange,
handleMemoryTypeChangeBefore,
memoryType,
handleUpdateMemory,
showTipsWhenMemoryModeBlockToLinear,
setShowTipsWhenMemoryModeBlockToLinear,
} = useMemory(id, data as LLMNodeType)
const renderTrigger = useCallback((open?: boolean) => {
@ -87,7 +91,7 @@ const MemorySystem = ({
</div>
<MemorySelector
value={memoryType}
onSelected={handleMemoryTypeChange}
onSelected={handleMemoryTypeChangeBefore}
readonly={readonly}
/>
</div>
@ -114,6 +118,20 @@ const MemorySystem = ({
</Collapse>
<Split className='mt-4' />
</div>
{
showTipsWhenMemoryModeBlockToLinear && (
<Confirm
isShow
type='info'
showCancel={false}
onCancel={() => setShowTipsWhenMemoryModeBlockToLinear(false)}
onConfirm={() => handleMemoryTypeChange(MemoryMode.linear)}
confirmText={t('workflow.nodes.common.memory.toLinearConfirmButton')}
title={t('workflow.nodes.common.memory.toLinearConfirmTitle')}
content={t('workflow.nodes.common.memory.toLinearConfirmDesc')}
/>
)
}
</>
)
}

View File

@ -18,6 +18,7 @@ type Props = {
nodeId: string
editorId?: string
currentPrompt?: string
isBasicMode?: boolean
}
const PromptGeneratorBtn: FC<Props> = ({
@ -26,6 +27,7 @@ const PromptGeneratorBtn: FC<Props> = ({
nodeId,
editorId,
currentPrompt,
isBasicMode,
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: GenRes) => {
@ -50,6 +52,8 @@ const PromptGeneratorBtn: FC<Props> = ({
nodeId={nodeId}
editorId={editorId}
currentPrompt={currentPrompt}
isBasicMode={isBasicMode}
hideTryIt
/>
)}
</div>

View File

@ -214,7 +214,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
availableNodes={availableNodesWithParent}
isSupportFileVar
instanceId={`${id}-chat-workflow-llm-prompt-editor-user`}
isMemorySupported={memoryType === MemoryMode.block}
/>
{inputs.memory?.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (

View File

@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
@ -29,6 +30,7 @@ const NodeSelector: FC<Props> = ({
onChange,
nodeType = BlockEnum.LLM,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const filteredNodes = useStore(useShallow((s) => {
@ -46,11 +48,14 @@ const NodeSelector: FC<Props> = ({
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
{currentNode && (
<div className={cn('flex h-8 w-[208px] cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-normal px-2 py-1 pl-3 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>{currentNode.data?.title}</div>
<div className={cn('flex h-8 w-[208px] cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-2 py-1 pl-3 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
<BlockIcon className={cn('mr-1.5 h-4 w-4 shrink-0')} type={currentNode.data?.type} />
<div className='system-sm-regular grow truncate text-components-input-text-filled'>{currentNode.data?.title}</div>
</div>
)}
{!currentNode && (
<div className={cn('flex h-8 w-[208px] cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-normal px-2 py-1 pl-3 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>Select node...</div>
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>{t('workflow.chatVariable.modal.selectNode')}</div>
<RiArrowDownSLine className='h-4 w-4 text-text-quaternary' />
</div>
)}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useRef } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
@ -16,6 +16,10 @@ import { checkKeys } from '@/utils/var'
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import VariableForm from '@/app/components/base/form/form-scenarios/variable'
import { useForm } from '../hooks'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export type ModalPropsType = {
className?: string
@ -46,7 +50,19 @@ const ChatVariableModal = ({
defaultValues,
})
const type = useTanstackStore(form.store, (s: any) => s.values.value_type)
const isPristine = useTanstackStore(form.store, (s: any) => s.fieldMeta?.model?.isPristine)
const { data: defaultModel } = useDefaultModel(ModelTypeEnum.textGeneration, !chatVar && type === ChatVarType.Memory && isPristine)
useEffect(() => {
if (defaultModel) {
form.setFieldValue('model', {
mode: 'chat',
name: defaultModel.model,
provider: defaultModel.provider.provider,
completion_params: {},
})
}
}, [defaultModel])
const checkVariableName = (value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
if (!isValid) {

View File

@ -17,7 +17,7 @@ export const useForm = (chatVar?: ConversationVariable | MemoryVariable, nodeSco
const valueSchema = useValueSchema()
const editInJSONSchema = useEditInJSONSchema()
const memorySchema = useMemorySchema(nodeScopeMemoryVariable)
const memoryDefaultValues = useMemoryDefaultValues(nodeScopeMemoryVariable)
const memoryDefaultValues = useMemoryDefaultValues(chatVar, nodeScopeMemoryVariable)
const formSchemas = useMemo(() => {
return [

View File

@ -1,11 +1,42 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type {
AnyFormApi,
} from '@tanstack/react-form'
import { FormTypeEnum } from '@/app/components/base/form/types'
import type { FormSchema } from '@/app/components/base/form/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import type { MemoryVariable } from '@/app/components/workflow/types'
export const useMemorySchema = (nodeScopeMemoryVariable?: { nodeId: string }) => {
const { t } = useTranslation()
const getTemplateSelfFormProps = useCallback((form: AnyFormApi) => {
const {
node_id,
} = form.state.values
return {
enablePromptGenerator: true,
withTopDivider: true,
editorId: 'memory-template',
nodeId: node_id,
}
}, [t])
const getInstructionSelfFormProps = useCallback((form: AnyFormApi) => {
const {
node_id,
} = form.state.values
return {
enablePromptGenerator: true,
placeholder: t('workflow.chatVariable.modal.updateInstructionsPlaceholder'),
editorId: 'memory-instruction',
nodeId: node_id,
}
}, [t])
const scopeSchema = [
{
name: 'scope',
@ -66,9 +97,7 @@ export const useMemorySchema = (nodeScopeMemoryVariable?: { nodeId: string }) =>
value: [ChatVarType.Memory],
},
],
selfFormProps: {
withTopDivider: true,
},
selfFormProps: getTemplateSelfFormProps,
},
{
name: 'instruction',
@ -80,10 +109,7 @@ export const useMemorySchema = (nodeScopeMemoryVariable?: { nodeId: string }) =>
value: [ChatVarType.Memory],
},
],
selfFormProps: {
enablePromptGenerator: true,
placeholder: t('workflow.chatVariable.modal.updateInstructionsPlaceholder'),
},
selfFormProps: getInstructionSelfFormProps,
},
{
name: 'strategy',
@ -242,7 +268,7 @@ export const useMemorySchema = (nodeScopeMemoryVariable?: { nodeId: string }) =>
] as FormSchema[]
}
export const useMemoryDefaultValues = (nodeScopeMemoryVariable?: { nodeId: string }) => {
export const useMemoryDefaultValues = (chatVar?: MemoryVariable, nodeScopeMemoryVariable?: { nodeId: string }) => {
return {
template: '',
instruction: '',
@ -252,8 +278,8 @@ export const useMemoryDefaultValues = (nodeScopeMemoryVariable?: { nodeId: strin
scope: nodeScopeMemoryVariable ? 'node' : 'app',
node_id: nodeScopeMemoryVariable?.nodeId || '',
term: 'session',
more: false,
model: '',
more: true,
model: undefined,
schedule_mode: 'sync',
end_user_visible: false,
end_user_editable: false,

View File

@ -74,8 +74,20 @@ const ChatVariablePanel = () => {
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(
['conversation', chatVar.name],
valueSelector,
allNodes,
)
}, [store])

View File

@ -334,6 +334,7 @@ export type Var = {
nodeId?: string
isRagVariable?: boolean
schemaType?: string
isMemoryVariable?: boolean
memoryVariableName?: string
memoryVariableNodeId?: string
}

View File

@ -183,6 +183,7 @@ const translation = {
memoryModel: 'Memory Model',
updateMethod: 'Update Method',
editableInWebApp: 'Editable in Web App',
selectNode: 'Select node...',
},
storedContent: 'Stored content',
updatedAt: 'Updated at ',
@ -382,6 +383,11 @@ const translation = {
desc: 'AI remembers specific information you define using custom templates',
empty: 'Please add a memory variable in the Prompt',
},
deleteNodeMemoryVariableConfirmTitle: 'Delete "{{name}}"?',
deleteNodeMemoryVariableConfirmDesc: 'This variable is currently in use. Deleting it may cause unexpected behavior. This action cannot be undone.',
toLinearConfirmTitle: 'Cannot Enable Linear Mode',
toLinearConfirmDesc: 'Linear mode is incompatible with memory variables. To use linear mode, please remove the memory variable from your prompt first.',
toLinearConfirmButton: 'Got it',
},
memories: {
title: 'Memories',

View File

@ -183,6 +183,7 @@ const translation = {
memoryModel: '记忆模型',
updateMethod: '更新方法',
editableInWebApp: '在 Web 应用中可编辑',
selectNode: '选择节点...',
},
storedContent: '存储内容',
updatedAt: '更新时间 ',
@ -382,6 +383,11 @@ const translation = {
desc: 'AI 会记住你定义的特定信息',
empty: '请在提示词中添加记忆变量以启用记忆块',
},
deleteNodeMemoryVariableConfirmTitle: '删除 "{{name}}"?',
deleteNodeMemoryVariableConfirmDesc: '该变量当前正在使用。删除它可能会导致意外行为。此操作无法撤销。',
toLinearConfirmTitle: '无法启用线性模式',
toLinearConfirmDesc: '线性模式与记忆变量不兼容。要使用线性模式,请先从提示词中删除记忆变量。',
toLinearConfirmButton: '知道了',
},
memories: {
title: '记忆',