add metadata
This commit is contained in:
parent
b36ef4d97b
commit
28e6971938
@ -0,0 +1,64 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
const AddCondition = ({
|
||||
handleAddCondition,
|
||||
}: Pick<MetadataShape, 'handleAddCondition'>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 3,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary'
|
||||
>
|
||||
<RiAddLine className='w-3.5 h-3.5' />
|
||||
Add Condition
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='w-[320px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
|
||||
<div className='p-2 pb-1'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
placeholder='Search metadata'
|
||||
/>
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<div className='flex items-center px-3 h-6 rounded-md system-sm-medium text-text-secondary cursor-pointer hover:bg-state-base-hover'>
|
||||
<div
|
||||
className='grow truncate'
|
||||
title='Language'
|
||||
onClick={() => handleAddCondition?.('language')}
|
||||
>
|
||||
Language
|
||||
</div>
|
||||
<div className='shrink-0 system-xs-regular text-text-tertiary'>string</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddCondition
|
@ -0,0 +1,65 @@
|
||||
import { useCallback } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
RiCalendarLine,
|
||||
RiCloseCircleFill,
|
||||
} from '@remixicon/react'
|
||||
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConditionDateProps = {
|
||||
value: string
|
||||
onChange: (date: string) => void
|
||||
}
|
||||
const ConditionDate = ({
|
||||
value,
|
||||
onChange,
|
||||
}: ConditionDateProps) => {
|
||||
const handleDateChange = useCallback((date?: dayjs.Dayjs) => {
|
||||
if (date)
|
||||
onChange(date.format('YYYY-MM-DD'))
|
||||
else
|
||||
onChange('')
|
||||
}, [onChange])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<div className='group flex items-center h-8'>
|
||||
<div
|
||||
className={cn(
|
||||
'grow',
|
||||
value ? 'text-text-secondary' : 'text-text-tertiary',
|
||||
)}
|
||||
>
|
||||
{value || 'Choose a time...'}
|
||||
</div>
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden group-hover:block w-4 h-4 cursor-pointer hover:text-components-input-text-filled',
|
||||
value && 'text-text-quaternary',
|
||||
)}
|
||||
onClick={() => handleDateChange()}
|
||||
/>
|
||||
<RiCalendarLine
|
||||
className={cn(
|
||||
'block group-hover:hidden shrink-0 w-4 h-4',
|
||||
value ? 'text-text-quaternary' : 'text-text-tertiary',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}, [value, handleDateChange])
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
value={dayjs()}
|
||||
onChange={handleDateChange}
|
||||
onClear={handleDateChange}
|
||||
renderTrigger={renderTrigger}
|
||||
>
|
||||
|
||||
</DatePicker>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionDate
|
@ -0,0 +1,121 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { comparisonOperatorNotRequireValue } from './utils'
|
||||
import ConditionOperator from './condition-operator'
|
||||
import ConditionValueMethod from './condition-value-method'
|
||||
import ConditionString from './condition-string'
|
||||
import ConditionNumber from './condition-number'
|
||||
import ConditionDate from './condition-date'
|
||||
import { useCondition } from './hooks'
|
||||
import type {
|
||||
HandleRemoveCondition,
|
||||
HandleUpdateCondition,
|
||||
MetadataFilteringCondition,
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConditionItemProps = {
|
||||
index: number
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
condition: MetadataFilteringCondition // condition may the condition of case or condition of sub variable
|
||||
onRemoveCondition?: HandleRemoveCondition
|
||||
onUpdateCondition?: HandleUpdateCondition
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
}
|
||||
const ConditionItem = ({
|
||||
index,
|
||||
className,
|
||||
disabled,
|
||||
condition,
|
||||
onRemoveCondition,
|
||||
}: ConditionItemProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const { getConditionVariableType } = useCondition()
|
||||
|
||||
const canChooseOperator = useMemo(() => {
|
||||
if (disabled)
|
||||
return false
|
||||
|
||||
return true
|
||||
}, [disabled])
|
||||
|
||||
const doRemoveCondition = useCallback(() => {
|
||||
onRemoveCondition?.(index)
|
||||
}, [onRemoveCondition, index])
|
||||
|
||||
return (
|
||||
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
'grow bg-components-input-bg-normal rounded-lg',
|
||||
isHovered && 'bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='flex items-center p-1'>
|
||||
<div className='grow w-0'>
|
||||
<div className='inline-flex items-center h-6 border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark rounded-md shadow-xs'>
|
||||
<div className='mr-0.5 system-xs-medium text-text-secondary'>Language</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>string</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-1 w-[1px] h-3 bg-divider-regular'></div>
|
||||
<ConditionOperator
|
||||
disabled={!canChooseOperator}
|
||||
variableType={MetadataFilteringVariableType.string}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center pl-1 pr-2 h-8'>
|
||||
<ConditionValueMethod
|
||||
valueMethod='variable'
|
||||
onValueMethodChange={() => {}}
|
||||
/>
|
||||
<div className='ml-1 mr-1.5 w-[1px] h-4 bg-divider-regular'></div>
|
||||
</div>
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && getConditionVariableType(condition.name) === MetadataFilteringVariableType.string && (
|
||||
<ConditionString
|
||||
onValueMethodChange={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && getConditionVariableType(condition.name) === MetadataFilteringVariableType.number && (
|
||||
<ConditionNumber
|
||||
onValueMethodChange={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && getConditionVariableType(condition.name) === MetadataFilteringVariableType.date && (
|
||||
<ConditionDate
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center ml-1 mt-1 w-6 h-6 rounded-lg cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={doRemoveCondition}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionItem
|
@ -0,0 +1,33 @@
|
||||
import ConditionValueMethod from './condition-value-method'
|
||||
import type { ConditionValueMethodProps } from './condition-value-method'
|
||||
import ConditionVariableSelector from './condition-variable-selector'
|
||||
|
||||
type ConditionNumberProps = {} & ConditionValueMethodProps
|
||||
const ConditionNumber = ({
|
||||
valueMethod,
|
||||
onValueMethodChange,
|
||||
}: ConditionNumberProps) => {
|
||||
return (
|
||||
<div className='flex items-center pl-1 pr-2 h-8'>
|
||||
<ConditionValueMethod
|
||||
valueMethod={valueMethod}
|
||||
onValueMethodChange={onValueMethodChange}
|
||||
/>
|
||||
<div className='ml-1 mr-1.5 w-[1px] h-4 bg-divider-regular'></div>
|
||||
{
|
||||
valueMethod === 'variable' && (
|
||||
<ConditionVariableSelector
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
valueMethod === 'constant' && (
|
||||
<input />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionNumber
|
@ -0,0 +1,98 @@
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
getOperators,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
} from './utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import type {
|
||||
ComparisonOperator,
|
||||
MetadataFilteringVariableType,
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionOperatorProps = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
variableType: MetadataFilteringVariableType
|
||||
value?: string
|
||||
onSelect: (value: ComparisonOperator) => void
|
||||
}
|
||||
const ConditionOperator = ({
|
||||
className,
|
||||
disabled,
|
||||
variableType,
|
||||
value,
|
||||
onSelect,
|
||||
}: ConditionOperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return getOperators(variableType).map((o) => {
|
||||
return {
|
||||
label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o,
|
||||
value: o,
|
||||
}
|
||||
})
|
||||
}, [t, variableType])
|
||||
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={disabled}
|
||||
>
|
||||
{
|
||||
selectedOption
|
||||
? selectedOption.label
|
||||
: t(`${i18nPrefix}.select`)
|
||||
}
|
||||
<RiArrowDownSLine className='ml-1 w-3.5 h-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex items-center px-3 py-1.5 h-7 text-[13px] font-medium text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onSelect(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionOperator
|
@ -0,0 +1,33 @@
|
||||
import ConditionValueMethod from './condition-value-method'
|
||||
import type { ConditionValueMethodProps } from './condition-value-method'
|
||||
import ConditionVariableSelector from './condition-variable-selector'
|
||||
|
||||
type ConditionStringProps = {} & ConditionValueMethodProps
|
||||
const ConditionString = ({
|
||||
valueMethod,
|
||||
onValueMethodChange,
|
||||
}: ConditionStringProps) => {
|
||||
return (
|
||||
<div className='flex items-center pl-1 pr-2 h-8'>
|
||||
<ConditionValueMethod
|
||||
valueMethod={valueMethod}
|
||||
onValueMethodChange={onValueMethodChange}
|
||||
/>
|
||||
<div className='ml-1 mr-1.5 w-[1px] h-4 bg-divider-regular'></div>
|
||||
{
|
||||
valueMethod === 'variable' && (
|
||||
<ConditionVariableSelector
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
valueMethod === 'constant' && (
|
||||
<input />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionString
|
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ConditionValueMethodProps = {
|
||||
valueMethod?: string
|
||||
onValueMethodChange: (v: string) => void
|
||||
}
|
||||
const options = [
|
||||
'variable',
|
||||
'constant',
|
||||
]
|
||||
const ConditionValueMethod = ({
|
||||
valueMethod = 'variable',
|
||||
onValueMethodChange,
|
||||
}: ConditionValueMethodProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{ mainAxis: 4, crossAxis: 0 }}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className='shrink-0'
|
||||
variant='ghost'
|
||||
size='small'
|
||||
>
|
||||
{capitalize(valueMethod)}
|
||||
<RiArrowDownSLine className='ml-[1px] w-3.5 h-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='p-1 w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option}
|
||||
className={cn(
|
||||
'flex items-center px-3 h-7 rounded-md hover:bg-state-base-hover cursor-pointer',
|
||||
'text-[13px] font-medium text-text-secondary',
|
||||
valueMethod === option && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
onValueMethodChange(option)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{capitalize(option)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionValueMethod
|
@ -0,0 +1,67 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionVariableSelectorProps = {
|
||||
valueSelector?: ValueSelector
|
||||
varType?: VarType
|
||||
availableNodes?: Node[]
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
onChange: (valueSelector: ValueSelector, varItem: Var) => void
|
||||
}
|
||||
|
||||
const ConditionVariableSelector = ({
|
||||
valueSelector = [],
|
||||
varType = VarType.string,
|
||||
availableNodes = [],
|
||||
nodesOutputVars = [],
|
||||
onChange,
|
||||
}: ConditionVariableSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
|
||||
<div className="cursor-pointer">
|
||||
<VariableTag
|
||||
valueSelector={valueSelector}
|
||||
varType={varType}
|
||||
availableNodes={availableNodes}
|
||||
isShort
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={nodesOutputVars}
|
||||
isSupportFileVar
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionVariableSelector
|
@ -0,0 +1,11 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const useCondition = () => {
|
||||
const getConditionVariableType = useCallback((name: string) => {
|
||||
return name
|
||||
}, [])
|
||||
|
||||
return {
|
||||
getConditionVariableType,
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { RiLoopLeftLine } from '@remixicon/react'
|
||||
import { useMemo } from 'react'
|
||||
import ConditionItem from './condition-item'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { LogicalOperator } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
type ConditionListProps = {
|
||||
disabled?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
} & Omit<MetadataShape, 'handleAddCondition'>
|
||||
const ConditionList = ({
|
||||
disabled,
|
||||
metadataFilteringConditions,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleUpdateCondition,
|
||||
nodesOutputVars = [],
|
||||
availableNodes = [],
|
||||
}: ConditionListProps) => {
|
||||
const { conditions, logical_operator } = metadataFilteringConditions
|
||||
|
||||
const conditionItemClassName = useMemo(() => {
|
||||
if (conditions.length < 2)
|
||||
return ''
|
||||
return logical_operator === LogicalOperator.and ? 'pl-[51px]' : 'pl-[42px]'
|
||||
}, [conditions.length, logical_operator])
|
||||
|
||||
return (
|
||||
<div className={cn('relative')}>
|
||||
{
|
||||
conditions.length > 1 && (
|
||||
<div className={cn(
|
||||
'absolute top-0 bottom-0 left-0 w-[60px]',
|
||||
)}>
|
||||
<div className='absolute top-4 bottom-4 left-[46px] w-2.5 border border-divider-deep rounded-l-[8px] border-r-0'></div>
|
||||
<div className='absolute top-1/2 -translate-y-1/2 right-0 w-4 h-[29px] bg-components-panel-bg'></div>
|
||||
<div
|
||||
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer select-none'
|
||||
onClick={() => handleToggleConditionLogicalOperator()}
|
||||
>
|
||||
{logical_operator.toUpperCase()}
|
||||
<RiLoopLeftLine className='ml-0.5 w-3 h-3' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
conditions.map((condition, index) => (
|
||||
<ConditionItem
|
||||
key={index}
|
||||
className={conditionItemClassName}
|
||||
disabled={disabled}
|
||||
condition={condition}
|
||||
onUpdateCondition={handleUpdateCondition}
|
||||
onRemoveCondition={handleRemoveCondition}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionList
|
@ -0,0 +1,61 @@
|
||||
import {
|
||||
ComparisonOperator,
|
||||
MetadataFilteringVariableType,
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
export const isEmptyRelatedOperator = (operator: ComparisonOperator) => {
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
|
||||
}
|
||||
|
||||
const notTranslateKey = [
|
||||
ComparisonOperator.equal, ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan, ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThan, ComparisonOperator.lessThanOrEqual,
|
||||
]
|
||||
|
||||
export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator) => {
|
||||
if (!operator)
|
||||
return false
|
||||
return !notTranslateKey.includes(operator)
|
||||
}
|
||||
|
||||
export const getOperators = (type?: MetadataFilteringVariableType) => {
|
||||
switch (type) {
|
||||
case MetadataFilteringVariableType.string:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case MetadataFilteringVariableType.number:
|
||||
return [
|
||||
ComparisonOperator.equal,
|
||||
ComparisonOperator.notEqual,
|
||||
ComparisonOperator.largerThan,
|
||||
ComparisonOperator.lessThan,
|
||||
ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThanOrEqual,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
default:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const comparisonOperatorNotRequireValue = (operator?: ComparisonOperator) => {
|
||||
if (!operator)
|
||||
return false
|
||||
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import { useState } from 'react'
|
||||
import MetadataTrigger from '../metadata-trigger'
|
||||
import MetadataFilterSelector from './metadata-filter-selector'
|
||||
import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
type MetadataFilterProps = {
|
||||
metadataFilterMode: MetadataFilteringModeEnum
|
||||
handleMetadataFilterModeChange: (mode: MetadataFilteringModeEnum) => void
|
||||
} & MetadataShape
|
||||
const MetadataFilter = ({
|
||||
metadataFilterMode,
|
||||
handleMetadataFilterModeChange,
|
||||
...restProps
|
||||
}: MetadataFilterProps) => {
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
disabled={metadataFilterMode === MetadataFilteringModeEnum.disabled}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
trigger={
|
||||
<div className='grow flex items-center justify-between pr-4'>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>
|
||||
metadata filtering
|
||||
</div>
|
||||
<Tooltip popupContent='Metadata filtering is the process of using metadata attributes (such as tags, categories, or access permissions) to refine and control the retrieval of relevant information within a system.' />
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<MetadataFilterSelector
|
||||
value={metadataFilterMode}
|
||||
onSelect={handleMetadataFilterModeChange}
|
||||
/>
|
||||
{
|
||||
metadataFilterMode === MetadataFilteringModeEnum.manual && (
|
||||
<MetadataTrigger {...restProps} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{
|
||||
metadataFilterMode === MetadataFilteringModeEnum.automatic && (
|
||||
<div className='body-xs-regular text-text-tertiary'>
|
||||
Automatically generate metadata filtering conditions based on Query Variable
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetadataFilter
|
@ -0,0 +1,98 @@
|
||||
import { useState } from 'react'
|
||||
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'
|
||||
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
type MetadataFilterSelectorProps = {
|
||||
value: MetadataFilteringModeEnum
|
||||
onSelect: (value: MetadataFilteringModeEnum) => void
|
||||
}
|
||||
const MetadataFilterSelector = ({
|
||||
value,
|
||||
onSelect,
|
||||
}: MetadataFilterSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const options = [
|
||||
{
|
||||
key: MetadataFilteringModeEnum.disabled,
|
||||
value: 'Disabled',
|
||||
desc: 'Not enabling metadata filtering',
|
||||
},
|
||||
{
|
||||
key: MetadataFilteringModeEnum.automatic,
|
||||
value: 'Automatic',
|
||||
desc: 'Automatically generate metadata filtering conditions based on user query',
|
||||
},
|
||||
{
|
||||
key: MetadataFilteringModeEnum.manual,
|
||||
value: 'Manual',
|
||||
desc: 'Manually add metadata filtering conditions',
|
||||
},
|
||||
]
|
||||
|
||||
const selectedOption = options.find(option => option.key === value)!
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
>
|
||||
{selectedOption.value}
|
||||
<RiArrowDownSLine className='w-3.5 h-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='p-1 bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className='flex p-2 pr-3 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onSelect(option.key)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='shrink-0 w-4'>
|
||||
{
|
||||
option.key === value && (
|
||||
<RiCheckLine className='w-4 h-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-sm-semibold text-text-secondary'>
|
||||
{option.value}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{option.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetadataFilterSelector
|
@ -0,0 +1,41 @@
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import AddCondition from './add-condition'
|
||||
import ConditionList from './condition-list'
|
||||
import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
type MetadataPanelProps = {
|
||||
onCancel: () => void
|
||||
} & MetadataShape
|
||||
const MetadataPanel = ({
|
||||
metadataFilteringConditions,
|
||||
onCancel,
|
||||
handleAddCondition,
|
||||
...restProps
|
||||
}: MetadataPanelProps) => {
|
||||
return (
|
||||
<div className='w-[420px] bg-components-panel-bg border-[0.5px] border-components-panel-border rounded-2xl shadow-2xl'>
|
||||
<div className='relative px-3 pt-3.5'>
|
||||
<div className='system-xl-semibold text-text-primary'>
|
||||
Metadata Filter Conditions
|
||||
</div>
|
||||
<div
|
||||
className='absolute right-2.5 bottom-0 flex items-center justify-center w-8 h-8 cursor-pointer'
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-1 py-2'>
|
||||
<div className='px-3 py-1'>
|
||||
<ConditionList
|
||||
metadataFilteringConditions={metadataFilteringConditions}
|
||||
{...restProps}
|
||||
/>
|
||||
<AddCondition handleAddCondition={handleAddCondition} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetadataPanel
|
@ -0,0 +1,48 @@
|
||||
import { useState } from 'react'
|
||||
import { RiFilter3Line } from '@remixicon/react'
|
||||
import MetadataPanel from './metadata-panel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
|
||||
const MetadataTrigger = ({
|
||||
metadataFilteringConditions,
|
||||
...restProps
|
||||
}: MetadataShape) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='left'
|
||||
offset={4}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
|
||||
<Button
|
||||
variant='secondary-accent'
|
||||
size='small'
|
||||
>
|
||||
<RiFilter3Line className='mr-1 w-3.5 h-3.5' />
|
||||
Conditions
|
||||
<div className='flex items-center ml-1 px-1 rounded-[5px] border border-divider-deep system-2xs-medium-uppercase text-text-tertiary'>
|
||||
{metadataFilteringConditions.conditions.length}
|
||||
</div>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<MetadataPanel
|
||||
metadataFilteringConditions={metadataFilteringConditions}
|
||||
onCancel={() => setOpen(false)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetadataTrigger
|
@ -30,6 +30,58 @@ export type SingleRetrievalConfig = {
|
||||
model: ModelConfig
|
||||
}
|
||||
|
||||
export enum LogicalOperator {
|
||||
and = 'and',
|
||||
or = 'or',
|
||||
}
|
||||
|
||||
export enum ComparisonOperator {
|
||||
contains = 'contains',
|
||||
notContains = 'not contains',
|
||||
startWith = 'start with',
|
||||
endWith = 'end with',
|
||||
is = 'is',
|
||||
isNot = 'is not',
|
||||
empty = 'empty',
|
||||
notEmpty = 'not empty',
|
||||
equal = '=',
|
||||
notEqual = '≠',
|
||||
largerThan = '>',
|
||||
lessThan = '<',
|
||||
largerThanOrEqual = '≥',
|
||||
lessThanOrEqual = '≤',
|
||||
isNull = 'is null',
|
||||
isNotNull = 'is not null',
|
||||
in = 'in',
|
||||
notIn = 'not in',
|
||||
allOf = 'all of',
|
||||
exists = 'exists',
|
||||
notExists = 'not exists',
|
||||
}
|
||||
|
||||
export enum MetadataFilteringModeEnum {
|
||||
disabled = 'disabled',
|
||||
automatic = 'automatic',
|
||||
manual = 'manual',
|
||||
}
|
||||
|
||||
export enum MetadataFilteringVariableType {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
date = 'date',
|
||||
}
|
||||
|
||||
export type MetadataFilteringCondition = {
|
||||
name: string
|
||||
comparison_operator: ComparisonOperator
|
||||
value?: string
|
||||
}
|
||||
|
||||
export type MetadataFilteringConditions = {
|
||||
logical_operator: LogicalOperator
|
||||
conditions: MetadataFilteringCondition[]
|
||||
}
|
||||
|
||||
export type KnowledgeRetrievalNodeType = CommonNodeType & {
|
||||
query_variable_selector: ValueSelector
|
||||
dataset_ids: string[]
|
||||
@ -37,4 +89,19 @@ export type KnowledgeRetrievalNodeType = CommonNodeType & {
|
||||
multiple_retrieval_config?: MultipleRetrievalConfig
|
||||
single_retrieval_config?: SingleRetrievalConfig
|
||||
_datasets?: DataSet[]
|
||||
metadata_filtering_mode?: MetadataFilteringModeEnum
|
||||
metadata_filtering_conditions?: MetadataFilteringConditions
|
||||
}
|
||||
|
||||
export type HandleAddCondition = (name: string) => void
|
||||
export type HandleRemoveCondition = (index: number) => void
|
||||
export type HandleUpdateCondition = (index: number, newCondition: MetadataFilteringCondition) => void
|
||||
export type HandleToggleConditionLogicalOperator = () => void
|
||||
|
||||
export type MetadataShape = {
|
||||
metadataFilteringConditions: MetadataFilteringConditions
|
||||
handleAddCondition: HandleAddCondition
|
||||
handleRemoveCondition: HandleRemoveCondition
|
||||
handleToggleConditionLogicalOperator: HandleToggleConditionLogicalOperator
|
||||
handleUpdateCondition: HandleUpdateCondition
|
||||
}
|
||||
|
@ -9,10 +9,20 @@ import { isEqual } from 'lodash-es'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import {
|
||||
useIsChatMode, useNodesReadOnly,
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import type { KnowledgeRetrievalNodeType, MultipleRetrievalConfig } from './types'
|
||||
import type {
|
||||
HandleAddCondition,
|
||||
HandleRemoveCondition,
|
||||
HandleToggleConditionLogicalOperator,
|
||||
HandleUpdateCondition,
|
||||
KnowledgeRetrievalNodeType,
|
||||
MetadataFilteringModeEnum,
|
||||
MultipleRetrievalConfig,
|
||||
} from './types'
|
||||
import { ComparisonOperator, LogicalOperator } from './types'
|
||||
import {
|
||||
getMultipleRetrievalConfig,
|
||||
getSelectedDatasetsMode,
|
||||
@ -202,7 +212,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
|
||||
const inputs = inputRef.current
|
||||
const datasetIds = inputs.dataset_ids
|
||||
if (datasetIds?.length > 0) {
|
||||
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } })
|
||||
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } as any })
|
||||
setSelectedDatasets(dataSetsWithDetail)
|
||||
}
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
@ -287,6 +297,43 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
|
||||
})
|
||||
}, [runInputData, setRunInputData])
|
||||
|
||||
const handleMetadataFilterModeChange = useCallback((newMode: MetadataFilteringModeEnum) => {
|
||||
setInputs(produce(inputRef.current, (draft) => {
|
||||
draft.metadata_filtering_mode = newMode
|
||||
}))
|
||||
}, [setInputs])
|
||||
|
||||
const handleAddCondition = useCallback<HandleAddCondition>((name) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.metadata_filtering_conditions?.conditions.push({
|
||||
name,
|
||||
comparison_operator: ComparisonOperator.is,
|
||||
})
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((index) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.metadata_filtering_conditions?.conditions.splice(index, 1)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const handleUpdateCondition = useCallback<HandleUpdateCondition>((index, newCondition) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.metadata_filtering_conditions!.conditions[index] = newCondition
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.metadata_filtering_conditions!.logical_operator = LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
@ -308,6 +355,11 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
|
||||
runResult,
|
||||
rerankModelOpen,
|
||||
setRerankModelOpen,
|
||||
handleMetadataFilterModeChange,
|
||||
handleUpdateCondition,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user