add metadata

This commit is contained in:
zxhlyh 2025-02-20 15:14:36 +08:00
parent b36ef4d97b
commit 28e6971938
17 changed files with 1062 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
import { useCallback } from 'react'
export const useCondition = () => {
const getConditionVariableType = useCallback((name: string) => {
return name
}, [])
return {
getConditionVariableType,
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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,
}
}