mirror of https://github.com/langgenius/dify.git
form
This commit is contained in:
parent
158da1ce6e
commit
01f0ee339e
|
|
@ -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 = ({
|
|||
<>
|
||||
<div className='group/collapse flex items-center'>
|
||||
<div
|
||||
className='ml-4 flex grow items-center'
|
||||
className={cn('ml-4 flex grow items-center', triggerClassName)}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCollapsedLocal(!collapsedMerged)
|
||||
|
|
|
|||
|
|
@ -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 = <T>(id: string, data: CommonNodeType<T>) => {
|
||||
|
|
@ -16,4 +18,28 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
|||
}
|
||||
}
|
||||
|
||||
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<CommonNodeType>) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data,
|
||||
})
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft])
|
||||
|
||||
return {
|
||||
getNodeData,
|
||||
handleNodeDataUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNodeCrud
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Node, 'id' | 'data'>
|
||||
const MemorySystem = ({
|
||||
id,
|
||||
data,
|
||||
}: MemoryProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { memory } = data as LLMNodeType
|
||||
const {
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
handleMemoryTypeChange,
|
||||
} = useMemory(id, data as LLMNodeType)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='py-4'>
|
||||
<Collapse
|
||||
disabled={!memory?.enabled}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
hideCollapseIcon
|
||||
triggerClassName='ml-0 system-sm-semibold-uppercase'
|
||||
trigger={
|
||||
collapseIcon => (
|
||||
<div className='flex grow items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<div className='system-sm-semibold-uppercase mr-0.5 text-text-secondary'>
|
||||
{t('workflow.nodes.common.errorHandle.title')}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={t('workflow.nodes.common.errorHandle.tip')}
|
||||
triggerClassName='w-4 h-4'
|
||||
/>
|
||||
{collapseIcon}
|
||||
</div>
|
||||
<MemorySelector
|
||||
value='linear'
|
||||
onSelected={handleMemoryTypeChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{
|
||||
(memory?.mode === 'linear' || !memory?.mode) && !collapsed && (
|
||||
<LinearMemory
|
||||
payload={memory as Memory}
|
||||
onChange={() => {
|
||||
console.log('onChange')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
</Collapse>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MemorySystem)
|
||||
|
|
@ -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<HTMLInputElement>) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-[13px] font-normal text-text-secondary'>{title}</div>
|
||||
<Input
|
||||
readOnly={readonly}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className='h-8 w-[200px]'
|
||||
type='text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex h-8 items-center space-x-2'>
|
||||
<Switch
|
||||
defaultValue={payload?.window?.enabled}
|
||||
onChange={handleWindowEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.windowSize`)}</div>
|
||||
</div>
|
||||
<div className='flex h-8 items-center space-x-2'>
|
||||
<Slider
|
||||
className='w-[144px]'
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={handleWindowSizeChange}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
<Input
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
wrapperClassName='w-12'
|
||||
className='appearance-none pr-0'
|
||||
type='number'
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={e => handleWindowSizeChange(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{canSetRoleName && (
|
||||
<div className='mt-4'>
|
||||
<div className='text-xs font-medium uppercase leading-6 text-text-tertiary'>{t(`${i18nPrefix}.conversationRoleName`)}</div>
|
||||
<div className='mt-1 space-y-2'>
|
||||
<RoleItem
|
||||
readonly={!!readonly}
|
||||
title={t(`${i18nPrefix}.user`)}
|
||||
value={payload.role_prefix?.user || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.user)}
|
||||
/>
|
||||
<RoleItem
|
||||
readonly={!!readonly}
|
||||
title={t(`${i18nPrefix}.assistant`)}
|
||||
value={payload.role_prefix?.assistant || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.assistant)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(LinearMemory)
|
||||
|
|
@ -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 (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setOpen(v => !v)
|
||||
}}>
|
||||
<Button
|
||||
size='small'
|
||||
>
|
||||
{selectedOption?.label}
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onSelected(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 w-4 shrink-0'>
|
||||
{
|
||||
value === option.value && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MemorySelector)
|
||||
|
|
@ -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<NodePanelProps<LLMNodeType>> = ({
|
|||
readonly={readOnly}
|
||||
config={{ data: inputs.memory }}
|
||||
onChange={handleMemoryChange}
|
||||
canSetRoleName={isCompletionModel}
|
||||
canSetRoleName={!isCompletionModel}
|
||||
/>
|
||||
<MemorySystem
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -340,6 +340,18 @@ const translation = {
|
|||
conversationRoleName: '对话角色名',
|
||||
user: '用户前缀',
|
||||
assistant: '助手前缀',
|
||||
disabled: {
|
||||
title: '禁用',
|
||||
desc: 'AI 不会记住任何之前的对话',
|
||||
},
|
||||
linear: {
|
||||
title: '线性',
|
||||
desc: 'AI 会记住对话的顺序和之前的消息',
|
||||
},
|
||||
block: {
|
||||
title: '记忆块',
|
||||
desc: 'AI 会记住你定义的特定信息',
|
||||
},
|
||||
},
|
||||
memories: {
|
||||
title: '记忆',
|
||||
|
|
|
|||
Loading…
Reference in New Issue