diff --git a/web/app/components/workflow/nodes/_base/components/collapse/index.tsx b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx
index 16fba88a25..82b43f98e3 100644
--- a/web/app/components/workflow/nodes/_base/components/collapse/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx
@@ -13,6 +13,7 @@ type CollapseProps = {
onCollapse?: (collapsed: boolean) => void
operations?: ReactNode
hideCollapseIcon?: boolean
+ triggerClassName?: string
}
const Collapse = ({
disabled,
@@ -22,6 +23,7 @@ const Collapse = ({
onCollapse,
operations,
hideCollapseIcon,
+ triggerClassName,
}: CollapseProps) => {
const [collapsedLocal, setCollapsedLocal] = useState(true)
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
@@ -42,7 +44,7 @@ const Collapse = ({
<>
{
if (!disabled) {
setCollapsedLocal(!collapsedMerged)
diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts
index 51d2fdb80c..6871be800c 100644
--- a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts
+++ b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts
@@ -1,3 +1,5 @@
+import { useCallback } from 'react'
+import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { CommonNodeType } from '@/app/components/workflow/types'
const useNodeCrud =
(id: string, data: CommonNodeType) => {
@@ -16,4 +18,28 @@ const useNodeCrud = (id: string, data: CommonNodeType) => {
}
}
+export const useNodeUpdate = (id: string) => {
+ const store = useStoreApi()
+ const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+
+ const getNodeData = useCallback(() => {
+ const { getNodes } = store.getState()
+ const nodes = getNodes()
+
+ return nodes.find(node => node.id === id)
+ }, [store, id])
+
+ const handleNodeDataUpdate = useCallback((data: Partial) => {
+ handleNodeDataUpdateWithSyncDraft({
+ id,
+ data,
+ })
+ }, [id, handleNodeDataUpdateWithSyncDraft])
+
+ return {
+ getNodeData,
+ handleNodeDataUpdate,
+ }
+}
+
export default useNodeCrud
diff --git a/web/app/components/workflow/nodes/llm/components/memory-system/hooks.ts b/web/app/components/workflow/nodes/llm/components/memory-system/hooks.ts
new file mode 100644
index 0000000000..cb5686a313
--- /dev/null
+++ b/web/app/components/workflow/nodes/llm/components/memory-system/hooks.ts
@@ -0,0 +1,40 @@
+import {
+ useCallback,
+ useMemo,
+ useState,
+} from 'react'
+import type { LLMNodeType } from '../../types'
+import { useNodeUpdate } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
+
+export const useMemory = (
+ id: string,
+ data: LLMNodeType,
+) => {
+ const { memory } = data
+ const initCollapsed = useMemo(() => {
+ if (!memory?.enabled)
+ return true
+
+ return false
+ }, [memory])
+ const [collapsed, setCollapsed] = useState(initCollapsed)
+ const { getNodeData } = useNodeUpdate(id)
+
+ const handleMemoryTypeChange = useCallback((value: string) => {
+ const nodeData = getNodeData()
+ console.log('nodeData', nodeData)
+
+ if (value === 'disabled')
+ console.log('disabled')
+ if (value === 'linear')
+ setCollapsed(true)
+ if (value === 'block')
+ setCollapsed(true)
+ }, [getNodeData])
+
+ return {
+ collapsed,
+ setCollapsed,
+ handleMemoryTypeChange,
+ }
+}
diff --git a/web/app/components/workflow/nodes/llm/components/memory-system/index.tsx b/web/app/components/workflow/nodes/llm/components/memory-system/index.tsx
new file mode 100644
index 0000000000..5b6dfaee96
--- /dev/null
+++ b/web/app/components/workflow/nodes/llm/components/memory-system/index.tsx
@@ -0,0 +1,76 @@
+import {
+ memo,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
+import type {
+ Node,
+} from '@/app/components/workflow/types'
+import Tooltip from '@/app/components/base/tooltip'
+import MemorySelector from './memory-selector'
+import LinearMemory from './linear-memory'
+import type { Memory } from '@/app/components/workflow/types'
+import type { LLMNodeType } from '../../types'
+import { useMemory } from './hooks'
+
+type MemoryProps = Pick
+const MemorySystem = ({
+ id,
+ data,
+}: MemoryProps) => {
+ const { t } = useTranslation()
+ const { memory } = data as LLMNodeType
+ const {
+ collapsed,
+ setCollapsed,
+ handleMemoryTypeChange,
+ } = useMemory(id, data as LLMNodeType)
+
+ return (
+ <>
+
+
(
+
+
+
+ {t('workflow.nodes.common.errorHandle.title')}
+
+
+ {collapseIcon}
+
+
+
+ )}
+ >
+ <>
+ {
+ (memory?.mode === 'linear' || !memory?.mode) && !collapsed && (
+ {
+ console.log('onChange')
+ }}
+ />
+ )
+ }
+ >
+
+
+ >
+ )
+}
+
+export default memo(MemorySystem)
diff --git a/web/app/components/workflow/nodes/llm/components/memory-system/linear-memory.tsx b/web/app/components/workflow/nodes/llm/components/memory-system/linear-memory.tsx
new file mode 100644
index 0000000000..4039cab007
--- /dev/null
+++ b/web/app/components/workflow/nodes/llm/components/memory-system/linear-memory.tsx
@@ -0,0 +1,179 @@
+import {
+ memo,
+ useCallback,
+} from 'react'
+import { produce } from 'immer'
+import { useTranslation } from 'react-i18next'
+import Switch from '@/app/components/base/switch'
+import Slider from '@/app/components/base/slider'
+import Input from '@/app/components/base/input'
+import type { Memory } from '@/app/components/workflow/types'
+import { MemoryRole } from '@/app/components/workflow/types'
+
+const WINDOW_SIZE_MIN = 1
+const WINDOW_SIZE_MAX = 100
+const WINDOW_SIZE_DEFAULT = 50
+const MEMORY_DEFAULT: Memory = {
+ window: { enabled: false, size: WINDOW_SIZE_DEFAULT },
+ query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}',
+}
+type RoleItemProps = {
+ readonly: boolean
+ title: string
+ value: string
+ onChange: (value: string) => void
+}
+const RoleItem = ({
+ readonly,
+ title,
+ value,
+ onChange,
+}: RoleItemProps) => {
+ const handleChange = useCallback((e: React.ChangeEvent) => {
+ onChange(e.target.value)
+ }, [onChange])
+ return (
+
+ )
+}
+
+type LinearMemoryProps = {
+ payload: Memory
+ readonly?: boolean
+ onChange: (payload: Memory) => void
+ canSetRoleName?: boolean
+}
+const LinearMemory = ({
+ payload,
+ readonly,
+ onChange,
+ canSetRoleName,
+}: LinearMemoryProps) => {
+ const i18nPrefix = 'workflow.nodes.common.memory'
+ const { t } = useTranslation()
+ const handleWindowEnabledChange = useCallback((enabled: boolean) => {
+ const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
+ if (!draft.window)
+ draft.window = { enabled: false, size: WINDOW_SIZE_DEFAULT }
+
+ draft.window.enabled = enabled
+ })
+
+ onChange(newPayload)
+ }, [payload, onChange])
+
+ const handleWindowSizeChange = useCallback((size: number | string) => {
+ const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
+ if (!draft.window)
+ draft.window = { enabled: true, size: WINDOW_SIZE_DEFAULT }
+ let limitedSize: null | string | number = size
+ if (limitedSize === '') {
+ limitedSize = null
+ }
+ else {
+ limitedSize = Number.parseInt(limitedSize as string, 10)
+ if (Number.isNaN(limitedSize))
+ limitedSize = WINDOW_SIZE_DEFAULT
+
+ if (limitedSize < WINDOW_SIZE_MIN)
+ limitedSize = WINDOW_SIZE_MIN
+
+ if (limitedSize > WINDOW_SIZE_MAX)
+ limitedSize = WINDOW_SIZE_MAX
+ }
+
+ draft.window.size = limitedSize as number
+ })
+ onChange(newPayload)
+ }, [payload, onChange])
+
+ const handleBlur = useCallback(() => {
+ if (!payload)
+ return
+
+ if (payload.window.size === '' || payload.window.size === null)
+ handleWindowSizeChange(WINDOW_SIZE_DEFAULT)
+ }, [handleWindowSizeChange, payload])
+ const handleRolePrefixChange = useCallback((role: MemoryRole) => {
+ return (value: string) => {
+ const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
+ if (!draft.role_prefix) {
+ draft.role_prefix = {
+ user: '',
+ assistant: '',
+ }
+ }
+ draft.role_prefix[role] = value
+ })
+ onChange(newPayload)
+ }
+ }, [payload, onChange])
+
+ return (
+ <>
+
+
+
+
{t(`${i18nPrefix}.windowSize`)}
+
+
+
+ handleWindowSizeChange(e.target.value)}
+ onBlur={handleBlur}
+ disabled={readonly || !payload.window?.enabled}
+ />
+
+
+ {canSetRoleName && (
+
+
{t(`${i18nPrefix}.conversationRoleName`)}
+
+
+
+
+
+ )}
+ >
+ )
+}
+
+export default memo(LinearMemory)
diff --git a/web/app/components/workflow/nodes/llm/components/memory-system/memory-selector.tsx b/web/app/components/workflow/nodes/llm/components/memory-system/memory-selector.tsx
new file mode 100644
index 0000000000..e93613c784
--- /dev/null
+++ b/web/app/components/workflow/nodes/llm/components/memory-system/memory-selector.tsx
@@ -0,0 +1,99 @@
+import {
+ memo,
+ useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ RiArrowDownSLine,
+ RiCheckLine,
+} from '@remixicon/react'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+ PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import Button from '@/app/components/base/button'
+
+type MemorySelectorProps = {
+ value: string
+ onSelected: (value: string) => void
+}
+const MemorySelector = ({
+ value,
+ onSelected,
+}: MemorySelectorProps) => {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+ const options = [
+ {
+ value: 'disabled',
+ label: t('workflow.nodes.common.memory.disabled.title'),
+ description: t('workflow.nodes.common.memory.disabled.desc'),
+ },
+ {
+ value: 'linear',
+ label: t('workflow.nodes.common.memory.linear.title'),
+ description: t('workflow.nodes.common.memory.linear.desc'),
+ },
+ {
+ value: 'block',
+ label: t('workflow.nodes.common.memory.block.title'),
+ description: t('workflow.nodes.common.memory.block.desc'),
+ },
+ ]
+ const selectedOption = options.find(option => option.value === value)
+
+ return (
+
+ {
+ e.stopPropagation()
+ e.nativeEvent.stopImmediatePropagation()
+ setOpen(v => !v)
+ }}>
+
+
+
+
+ {
+ options.map(option => (
+
{
+ e.stopPropagation()
+ e.nativeEvent.stopImmediatePropagation()
+ onSelected(option.value)
+ setOpen(false)
+ }}
+ >
+
+ {
+ value === option.value && (
+
+ )
+ }
+
+
+
{option.label}
+
{option.description}
+
+
+ ))
+ }
+
+
+
+ )
+}
+
+export default memo(MemorySelector)
diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx
index 1206e58734..827b44000f 100644
--- a/web/app/components/workflow/nodes/llm/panel.tsx
+++ b/web/app/components/workflow/nodes/llm/panel.tsx
@@ -21,6 +21,7 @@ import Switch from '@/app/components/base/switch'
import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import Toast from '@/app/components/base/toast'
+import MemorySystem from './components/memory-system'
const i18nPrefix = 'workflow.nodes.llm'
@@ -224,7 +225,11 @@ const Panel: FC> = ({
readonly={readOnly}
config={{ data: inputs.memory }}
onChange={handleMemoryChange}
- canSetRoleName={isCompletionModel}
+ canSetRoleName={!isCompletionModel}
+ />
+
>
)}
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx
index dbc1c566ae..41f7f4c3d2 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx
@@ -1,6 +1,7 @@
import React, { useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
+import { v4 as uuid4 } from 'uuid'
import {
useForm as useTanstackForm,
useStore as useTanstackStore,
@@ -63,26 +64,27 @@ const ChatVariableModal = ({
}) || { isCheckValidated: false, values: {} }
const {
name,
- type,
+ value_type,
value,
+ editInJSON,
+ ...rest
} = values
- console.log(values, 'xxx')
if (!isCheckValidated)
return
if (!checkVariableName(name))
return
if (!chatVar && varList.some(chatVar => chatVar.name === name))
return notify({ type: 'error', message: 'name is existed' })
- if (type === ChatVarType.Object && value.some((item: any) => !item.key && !!item.value))
+ if (value_type === ChatVarType.Object && value.some((item: any) => !item.key && !!item.value))
return notify({ type: 'error', message: 'object key can not be empty' })
- // onSave({
- // id: chatVar ? chatVar.id : uuid4(),
- // name,
- // value_type: type,
- // value: values,
- // description,
- // })
+ onSave({
+ id: chatVar ? chatVar.id : uuid4(),
+ name,
+ value_type,
+ value: editInJSON ? JSON.parse(value) : value,
+ ...rest,
+ })
onClose()
}, [onClose, notify, t, varList, chatVar, checkVariableName])
diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts
index 82ef97c30f..e47684bfd7 100644
--- a/web/app/components/workflow/types.ts
+++ b/web/app/components/workflow/types.ts
@@ -173,7 +173,7 @@ export type ConversationVariable = {
name: string
value_type: ChatVarType
value: any
- description: string
+ description?: string
} & MemoryVariable
export type GlobalVariable = {
@@ -259,12 +259,15 @@ export type RolePrefix = {
}
export type Memory = {
+ enabled?: boolean
role_prefix?: RolePrefix
window: {
enabled: boolean
size: number | string | null
}
query_prompt_template: string
+ block_id?: string
+ mode?: 'linear' | 'block'
}
export enum VarType {
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index f4353da24e..ee843df7ad 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -340,6 +340,18 @@ const translation = {
conversationRoleName: 'Conversation Role Name',
user: 'User prefix',
assistant: 'Assistant prefix',
+ disabled: {
+ title: 'Disabled',
+ desc: 'AI won\'t remember anything from previous conversations',
+ },
+ linear: {
+ title: 'Linear',
+ desc: 'AI remembers the conversation flow and previous messages in order',
+ },
+ block: {
+ title: 'Memory Block',
+ desc: 'AI remembers specific information you define using custom templates',
+ },
},
memories: {
title: 'Memories',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index daaba921ff..6f5ea409b1 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -340,6 +340,18 @@ const translation = {
conversationRoleName: '对话角色名',
user: '用户前缀',
assistant: '助手前缀',
+ disabled: {
+ title: '禁用',
+ desc: 'AI 不会记住任何之前的对话',
+ },
+ linear: {
+ title: '线性',
+ desc: 'AI 会记住对话的顺序和之前的消息',
+ },
+ block: {
+ title: '记忆块',
+ desc: 'AI 会记住你定义的特定信息',
+ },
},
memories: {
title: '记忆',