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 ( +
+
{title}
+ +
+ ) +} + +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: '记忆',