mirror of https://github.com/langgenius/dify.git
memory popup
This commit is contained in:
parent
f6623423dd
commit
0b1445aed5
|
|
@ -61,6 +61,8 @@ import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
|||
import { CustomTextNode } from './plugins/custom-text/node'
|
||||
import OnBlurBlock from './plugins/on-blur-or-focus-block'
|
||||
import UpdateBlock from './plugins/update-block'
|
||||
import MemoryPopupPlugin from './plugins/memory-popup-plugin'
|
||||
|
||||
import { textToEditorState } from './utils'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
|
|
@ -103,6 +105,7 @@ export type PromptEditorProps = {
|
|||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
isMemorySupported?: boolean
|
||||
}
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
|
|
@ -128,6 +131,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
isMemorySupported,
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const initialConfig = {
|
||||
|
|
@ -198,6 +202,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{isMemorySupported && (
|
||||
<MemoryPopupPlugin instanceId={instanceId} />
|
||||
)}
|
||||
<ComponentPickerBlock
|
||||
triggerString='/'
|
||||
contextBlock={contextBlock}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react'
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
} 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 cn from '@/utils/classnames'
|
||||
|
||||
export type MemoryPopupProps = {
|
||||
className?: string
|
||||
container?: Element | null
|
||||
instanceId?: string
|
||||
}
|
||||
|
||||
export default function MemoryPopupPlugin({
|
||||
className,
|
||||
container,
|
||||
instanceId,
|
||||
}: MemoryPopupProps) {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const portalRef = useRef<HTMLDivElement | null>(null)
|
||||
const lastSelectionRef = useRef<Range | null>(null)
|
||||
|
||||
const containerEl = useMemo(() => container ?? (typeof document !== 'undefined' ? document.body : null), [container])
|
||||
|
||||
const useContainer = !!containerEl && containerEl !== document.body
|
||||
|
||||
const { refs, floatingStyles, isPositioned } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
offset(0), // fix hide cursor
|
||||
shift({
|
||||
padding: 8,
|
||||
altBoundary: true,
|
||||
}),
|
||||
flip(),
|
||||
size({
|
||||
apply({ availableWidth, availableHeight, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
maxWidth: `${Math.min(400, availableWidth)}px`,
|
||||
maxHeight: `${Math.min(300, availableHeight)}px`,
|
||||
overflow: 'auto',
|
||||
})
|
||||
},
|
||||
padding: 8,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
})
|
||||
|
||||
const openPortal = useCallback(() => {
|
||||
const domSelection = window.getSelection()
|
||||
let range: Range | null = null
|
||||
if (domSelection && domSelection.rangeCount > 0)
|
||||
range = domSelection.getRangeAt(0).cloneRange()
|
||||
else
|
||||
range = lastSelectionRef.current
|
||||
|
||||
if (range) {
|
||||
const rects = range.getClientRects()
|
||||
let rect: DOMRect | null = null
|
||||
|
||||
if (rects && rects.length)
|
||||
rect = rects[rects.length - 1]
|
||||
|
||||
else
|
||||
rect = range.getBoundingClientRect()
|
||||
|
||||
if (rect.width === 0 && rect.height === 0) {
|
||||
const root = editor.getRootElement()
|
||||
if (root) {
|
||||
const sc = range.startContainer
|
||||
const node = sc.nodeType === Node.ELEMENT_NODE
|
||||
? sc as Element
|
||||
: (sc.parentElement || root)
|
||||
|
||||
rect = node.getBoundingClientRect()
|
||||
|
||||
if (rect.width === 0 && rect.height === 0)
|
||||
rect = root.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
|
||||
if (rect && !(rect.top === 0 && rect.left === 0 && rect.width === 0 && rect.height === 0)) {
|
||||
const virtualEl = {
|
||||
getBoundingClientRect() {
|
||||
return rect!
|
||||
},
|
||||
}
|
||||
refs.setReference(virtualEl as Element)
|
||||
}
|
||||
}
|
||||
|
||||
setOpen(true)
|
||||
}, [setOpen])
|
||||
|
||||
const closePortal = useCallback(() => {
|
||||
setOpen(false)
|
||||
}, [setOpen])
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
|
||||
openPortal()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
const domSelection = window.getSelection()
|
||||
if (domSelection && domSelection.rangeCount > 0)
|
||||
lastSelectionRef.current = domSelection.getRangeAt(0).cloneRange()
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open)
|
||||
return
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!portalRef.current)
|
||||
return
|
||||
if (!portalRef.current.contains(e.target as Node))
|
||||
closePortal()
|
||||
}
|
||||
document.addEventListener('mousedown', onMouseDown, false)
|
||||
return () => document.removeEventListener('mousedown', onMouseDown, false)
|
||||
}, [open, closePortal])
|
||||
|
||||
if (!open || !containerEl)
|
||||
return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={(node) => {
|
||||
portalRef.current = node
|
||||
refs.setFloating(node)
|
||||
}}
|
||||
className={cn(
|
||||
useContainer ? '' : 'z-[999999]',
|
||||
'absolute rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...floatingStyles,
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
Memory Popup
|
||||
</div>,
|
||||
containerEl,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import { $insertNodes } from 'lexical'
|
||||
import {
|
||||
$insertNodes,
|
||||
} from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { textToEditorState } from '../utils'
|
||||
import { CustomTextNode } from './custom-text/node'
|
||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
||||
import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from '../../../workflow/nodes/_base/components/prompt/add-memory-button'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
||||
|
|
@ -36,6 +39,18 @@ const UpdateBlock = ({
|
|||
}
|
||||
})
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId) {
|
||||
editor.focus()
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode('')
|
||||
$insertNodes([textNode])
|
||||
|
||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const WorkflowVariableBlockComponent = ({
|
|||
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
|
||||
const node = localWorkflowNodesMap![variables[isRagVar ? 1 : 0]]
|
||||
const isEnv = isENV(variables)
|
||||
const isChatVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type !== 'memory')
|
||||
// const isChatVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type !== 'memory')
|
||||
const isMemoryVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type === 'memory')
|
||||
const isException = isExceptionVariable(varName, node?.type)
|
||||
let variableValid = true
|
||||
|
|
@ -74,7 +74,7 @@ const WorkflowVariableBlockComponent = ({
|
|||
if (environmentVariables)
|
||||
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||
}
|
||||
else if (isChatVar) {
|
||||
else if (isConversationVar(variables)) {
|
||||
if (conversationVariables)
|
||||
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
}
|
||||
|
||||
const AddMemoryButton = ({ onAddMemory }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='ml-1.5 mt-2.5'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className='text-text-tertiary'
|
||||
onClick={onAddMemory}
|
||||
>
|
||||
<Memory className='h-3.5 w-3.5' />
|
||||
<span className='ml-1'>{t('workflow.nodes.llm.memory.addButton')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddMemoryButton
|
||||
|
|
@ -36,8 +36,7 @@ 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 Button from '@/app/components/base/button'
|
||||
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import AddMemoryButton, { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from './add-memory-button'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
|
@ -157,6 +156,11 @@ const Editor: FC<Props> = ({
|
|||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
const handleAddMemory = () => {
|
||||
setFocus()
|
||||
eventEmitter?.emit({ type: MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, instanceId } as any)
|
||||
}
|
||||
|
||||
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)}>
|
||||
|
|
@ -296,18 +300,12 @@ const Editor: FC<Props> = ({
|
|||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isMemorySupported
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
</div>
|
||||
{isMemorySupported && isFocus && (
|
||||
<div className='pl-1.5'>
|
||||
<Button variant='ghost' size='small' className='text-text-tertiary'>
|
||||
<Memory className='h-3.5 w-3.5' />
|
||||
<span className='ml-1'>{t('workflow.nodes.llm.memory.addButton')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isMemorySupported && <AddMemoryButton onAddMemory={handleAddMemory} />}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
isSupportFileVar
|
||||
instanceId={`${id}-chat-workflow-llm-prompt-editor-user`}
|
||||
/>
|
||||
|
||||
{inputs.memory.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue