feat: upgrade knowledge metadata (#16063)

Support filter knowledge by metadata.

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: NFish <douxc512@gmail.com>
This commit is contained in:
zxhlyh
2025-03-18 11:01:06 +08:00
committed by GitHub
parent 475b8d731e
commit 20376ca951
72 changed files with 4775 additions and 101 deletions

View File

@@ -0,0 +1,95 @@
import {
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
} from '@remixicon/react'
import MetadataIcon from './metadata-icon'
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'
import type { MetadataInDoc } from '@/models/datasets'
const AddCondition = ({
metadataList,
handleAddCondition,
}: Pick<MetadataShape, 'handleAddCondition' | 'metadataList'>) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('')
const filteredMetadataList = useMemo(() => {
return metadataList?.filter(metadata => metadata.name.includes(searchText))
}, [metadataList, searchText])
const handleAddConditionWrapped = useCallback((item: MetadataInDoc) => {
handleAddCondition?.(item)
setOpen(false)
}, [handleAddCondition])
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' />
{t('workflow.nodes.knowledgeRetrieval.metadata.panel.add')}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<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={t('workflow.nodes.knowledgeRetrieval.metadata.panel.search')}
value={searchText}
onChange={e => setSearchText(e.target.value)}
/>
</div>
<div className='p-1'>
{
filteredMetadataList?.map(metadata => (
<div
key={metadata.name}
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='mr-1 p-[1px]'>
<MetadataIcon type={metadata.type} />
</div>
<div
className='grow truncate'
title={metadata.name}
onClick={() => handleAddConditionWrapped(metadata)}
>
{metadata.name}
</div>
<div className='shrink-0 system-xs-regular text-text-tertiary'>{metadata.type}</div>
</div>
))
}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default AddCondition

View File

@@ -0,0 +1,91 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { VarType } from '@/app/components/workflow/types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
type ConditionCommonVariableSelectorProps = {
variables?: { name: string; type: string }[]
value?: string | number
varType?: VarType
onChange: (v: string) => void
}
const ConditionCommonVariableSelector = ({
variables = [],
value,
onChange,
varType,
}: ConditionCommonVariableSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const selected = variables.find(v => v.name === value)
const handleChange = useCallback((v: string) => {
onChange(v)
setOpen(false)
}, [onChange])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger asChild onClick={() => {
if (!variables.length) return
setOpen(!open)
}}>
<div className="grow flex items-center cursor-pointer h-6">
{
selected && (
<div className='inline-flex items-center pl-[5px] pr-1.5 h-6 text-text-secondary rounded-md system-xs-medium border-[0.5px] border-components-panel-border-subtle shadow-xs bg-components-badge-white-to-dark'>
<Variable02 className='mr-1 w-3.5 h-3.5 text-text-accent' />
{selected.name}
</div>
)
}
{
!selected && (
<>
<div className='grow flex items-center text-components-input-text-placeholder system-sm-regular'>
<Variable02 className='mr-1 w-4 h-4' />
{t('workflow.nodes.knowledgeRetrieval.metadata.panel.select')}
</div>
<div className='shrink-0 flex items-center px-[5px] h-5 border border-divider-deep rounded-[5px] system-2xs-medium text-text-tertiary'>
{varType}
</div>
</>
)
}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className='p-1 w-[200px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
{
variables.map(v => (
<div
key={v.name}
className='flex items-center px-2 h-6 cursor-pointer rounded-md text-text-secondary system-xs-medium hover:bg-state-base-hover'
onClick={() => handleChange(v.name)}
>
<Variable02 className='mr-1 w-4 h-4 text-text-accent' />
{v.name}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ConditionCommonVariableSelector

View File

@@ -0,0 +1,86 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import {
RiCalendarLine,
RiCloseCircleFill,
} from '@remixicon/react'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
import cn from '@/utils/classnames'
import { useAppContext } from '@/context/app-context'
type ConditionDateProps = {
value?: number
onChange: (date?: number) => void
}
const ConditionDate = ({
value,
onChange,
}: ConditionDateProps) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const handleDateChange = useCallback((date?: dayjs.Dayjs) => {
if (date)
onChange(date.unix())
else
onChange()
}, [onChange])
const renderTrigger = useCallback(({
handleClickTrigger,
}: TriggerProps) => {
return (
<div className='group flex items-center' onClick={handleClickTrigger}>
<div
className={cn(
'grow flex items-center mr-0.5 px-1 h-6 system-sm-regular cursor-pointer',
value ? 'text-text-secondary' : 'text-text-tertiary',
)}
>
{
value
? dayjs(value * 1000).tz(timezone).format('MMMM DD YYYY HH:mm A')
: t('workflow.nodes.knowledgeRetrieval.metadata.panel.datePlaceholder')
}
</div>
{
value && (
<RiCloseCircleFill
className={cn(
'hidden group-hover:block shrink-0 w-4 h-4 cursor-pointer hover:text-components-input-text-filled',
value && 'text-text-quaternary',
)}
onClick={(e) => {
e.stopPropagation()
handleDateChange()
}}
/>
)
}
<RiCalendarLine
className={cn(
'block shrink-0 w-4 h-4',
value ? 'text-text-quaternary' : 'text-text-tertiary',
value && 'group-hover:hidden',
)}
/>
</div>
)
}, [value, handleDateChange, timezone, t])
return (
<div className='px-2 py-1 h-8'>
<DatePicker
timezone={timezone}
value={value ? dayjs(value * 1000) : undefined}
onChange={handleDateChange}
onClear={handleDateChange}
renderTrigger={renderTrigger}
/>
</div>
)
}
export default ConditionDate

View File

@@ -0,0 +1,192 @@
import {
useCallback,
useMemo,
useState,
} from 'react'
import {
RiDeleteBinLine,
} from '@remixicon/react'
import MetadataIcon from '../metadata-icon'
import {
COMMON_VARIABLE_REGEX,
VARIABLE_REGEX,
comparisonOperatorNotRequireValue,
} from './utils'
import ConditionOperator from './condition-operator'
import ConditionString from './condition-string'
import ConditionNumber from './condition-number'
import ConditionDate from './condition-date'
import type {
ComparisonOperator,
HandleRemoveCondition,
HandleUpdateCondition,
MetadataFilteringCondition,
MetadataShape,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import cn from '@/utils/classnames'
type ConditionItemProps = {
className?: string
disabled?: boolean
condition: MetadataFilteringCondition // condition may the condition of case or condition of sub variable
onRemoveCondition?: HandleRemoveCondition
onUpdateCondition?: HandleUpdateCondition
} & Pick<MetadataShape, 'metadataList' | 'availableStringVars' | 'availableStringNodesWithParent' | 'availableNumberVars' | 'availableNumberNodesWithParent' | 'isCommonVariable' | 'availableCommonStringVars' | 'availableCommonNumberVars'>
const ConditionItem = ({
className,
disabled,
condition,
onRemoveCondition,
onUpdateCondition,
metadataList = [],
availableStringVars = [],
availableStringNodesWithParent = [],
availableNumberVars = [],
availableNumberNodesWithParent = [],
isCommonVariable,
availableCommonStringVars = [],
availableCommonNumberVars = [],
}: ConditionItemProps) => {
const [isHovered, setIsHovered] = useState(false)
const canChooseOperator = useMemo(() => {
if (disabled)
return false
return true
}, [disabled])
const doRemoveCondition = useCallback(() => {
onRemoveCondition?.(condition.id)
}, [onRemoveCondition, condition.id])
const currentMetadata = useMemo(() => {
return metadataList.find(metadata => metadata.name === condition.name)
}, [metadataList, condition.name])
const handleConditionOperatorChange = useCallback((operator: ComparisonOperator) => {
onUpdateCondition?.(
condition.id,
{
...condition,
value: comparisonOperatorNotRequireValue(condition.comparison_operator) ? undefined : condition.value,
comparison_operator: operator,
})
}, [onUpdateCondition, condition])
const valueAndValueMethod = useMemo(() => {
if (
(currentMetadata?.type === MetadataFilteringVariableType.string || currentMetadata?.type === MetadataFilteringVariableType.number)
&& typeof condition.value === 'string'
) {
const regex = isCommonVariable ? COMMON_VARIABLE_REGEX : VARIABLE_REGEX
const matchedStartNumber = isCommonVariable ? 2 : 3
const matched = condition.value.match(regex)
if (matched?.length) {
return {
value: matched[0].slice(matchedStartNumber, -matchedStartNumber),
valueMethod: 'variable',
}
}
else {
return {
value: condition.value,
valueMethod: 'constant',
}
}
}
return {
value: condition.value,
valueMethod: 'constant',
}
}, [currentMetadata, condition.value, isCommonVariable])
const [localValueMethod, setLocalValueMethod] = useState(valueAndValueMethod.valueMethod)
const handleValueMethodChange = useCallback((v: string) => {
setLocalValueMethod(v)
onUpdateCondition?.(condition.id, { ...condition, value: undefined })
}, [condition, onUpdateCondition])
const handleValueChange = useCallback((v: any) => {
onUpdateCondition?.(condition.id, { ...condition, value: v })
}, [condition, onUpdateCondition])
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 pl-1 pr-1.5 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 p-[1px]'>
<MetadataIcon type={currentMetadata?.type} className='w-3 h-3' />
</div>
<div className='mr-0.5 system-xs-medium text-text-secondary'>{currentMetadata?.name}</div>
<div className='system-xs-regular text-text-tertiary'>{currentMetadata?.type}</div>
</div>
</div>
<div className='mx-1 w-[1px] h-3 bg-divider-regular'></div>
<ConditionOperator
disabled={!canChooseOperator}
variableType={currentMetadata?.type || MetadataFilteringVariableType.string}
value={condition.comparison_operator}
onSelect={handleConditionOperatorChange}
/>
</div>
<div className='border-t border-t-divider-subtle'>
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && currentMetadata?.type === MetadataFilteringVariableType.string && (
<ConditionString
valueMethod={localValueMethod}
onValueMethodChange={handleValueMethodChange}
nodesOutputVars={availableStringVars}
availableNodes={availableStringNodesWithParent}
value={valueAndValueMethod.value as string}
onChange={handleValueChange}
isCommonVariable={isCommonVariable}
commonVariables={availableCommonStringVars}
/>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && currentMetadata?.type === MetadataFilteringVariableType.number && (
<ConditionNumber
valueMethod={localValueMethod}
onValueMethodChange={handleValueMethodChange}
nodesOutputVars={availableNumberVars}
availableNodes={availableNumberNodesWithParent}
value={valueAndValueMethod.value}
onChange={handleValueChange}
isCommonVariable={isCommonVariable}
commonVariables={availableCommonNumberVars}
/>
)
}
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && currentMetadata?.type === MetadataFilteringVariableType.time && (
<ConditionDate
value={condition.value as number}
onChange={handleValueChange}
/>
)
}
</div>
</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,88 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx'
import type {
Node,
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import Input from '@/app/components/base/input'
type ConditionNumberProps = {
value?: string | number
onChange: (value?: string | number) => void
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
isCommonVariable?: boolean
commonVariables: { name: string, type: string }[]
} & ConditionValueMethodProps
const ConditionNumber = ({
value,
onChange,
valueMethod,
onValueMethodChange,
nodesOutputVars,
availableNodes,
isCommonVariable,
commonVariables,
}: ConditionNumberProps) => {
const { t } = useTranslation()
const handleVariableValueChange = useCallback((v: ValueSelector) => {
onChange(`{{#${v.join('.')}#}}`)
}, [onChange])
const handleCommonVariableValueChange = useCallback((v: string) => {
onChange(`{{${v}}}`)
}, [onChange])
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' && !isCommonVariable && (
<ConditionVariableSelector
valueSelector={value ? (value as string).split('.') : []}
onChange={handleVariableValueChange}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
varType={VarType.number}
/>
)
}
{
valueMethod === 'variable' && isCommonVariable && (
<ConditionCommonVariableSelector
variables={commonVariables}
value={value}
onChange={handleCommonVariableValueChange}
varType={VarType.number}
/>
)
}
{
valueMethod === 'constant' && (
<Input
className='bg-transparent hover:bg-transparent outline-none border-none focus:shadow-none focus:bg-transparent'
value={value}
onChange={(e) => {
const v = e.target.value
onChange(v ? Number(e.target.value) : undefined)
}}
placeholder={t('workflow.nodes.knowledgeRetrieval.metadata.panel.placeholder')}
type='number'
/>
)
}
</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,84 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx'
import type {
Node,
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import Input from '@/app/components/base/input'
import { VarType } from '@/app/components/workflow/types'
type ConditionStringProps = {
value?: string
onChange: (value: string) => void
nodesOutputVars: NodeOutPutVar[]
availableNodes: Node[]
isCommonVariable?: boolean
commonVariables: { name: string, type: string }[]
} & ConditionValueMethodProps
const ConditionString = ({
value,
onChange,
valueMethod = 'constant',
onValueMethodChange,
nodesOutputVars,
availableNodes,
isCommonVariable,
commonVariables,
}: ConditionStringProps) => {
const { t } = useTranslation()
const handleVariableValueChange = useCallback((v: ValueSelector) => {
onChange(`{{#${v.join('.')}#}}`)
}, [onChange])
const handleCommonVariableValueChange = useCallback((v: string) => {
onChange(`{{${v}}}`)
}, [onChange])
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' && !isCommonVariable && (
<ConditionVariableSelector
valueSelector={value ? value!.split('.') : []}
onChange={handleVariableValueChange}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
varType={VarType.string}
/>
)
}
{
valueMethod === 'variable' && isCommonVariable && (
<ConditionCommonVariableSelector
variables={commonVariables}
value={value}
onChange={handleCommonVariableValueChange}
varType={VarType.string}
/>
)
}
{
valueMethod === 'constant' && (
<Input
className='bg-transparent hover:bg-transparent outline-none border-none focus:shadow-none focus:bg-transparent'
value={value}
onChange={e => onChange(e.target.value)}
placeholder={t('workflow.nodes.knowledgeRetrieval.metadata.panel.placeholder')}
/>
)
}
</div>
)
}
export default ConditionString

View File

@@ -0,0 +1,71 @@
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 asChild 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={() => {
if (valueMethod === option)
return
onValueMethodChange(option)
setOpen(false)
}}
>
{capitalize(option)}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ConditionValueMethod

View File

@@ -0,0 +1,92 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
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'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
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 { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleChange = useCallback((valueSelector: ValueSelector, varItem: Var) => {
onChange(valueSelector, varItem)
setOpen(false)
}, [onChange])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger asChild onClick={() => setOpen(!open)}>
<div className="grow flex items-center cursor-pointer h-6">
{
!!valueSelector.length && (
<VariableTag
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
isShort
/>
)
}
{
!valueSelector.length && (
<>
<div className='grow flex items-center text-components-input-text-placeholder system-sm-regular'>
<Variable02 className='mr-1 w-4 h-4' />
{t('workflow.nodes.knowledgeRetrieval.metadata.panel.select')}
</div>
<div className='shrink-0 flex items-center px-[5px] h-5 border border-divider-deep rounded-[5px] system-2xs-medium text-text-tertiary'>
{varType}
</div>
</>
)
}
</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={handleChange}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ConditionVariableSelector

View File

@@ -0,0 +1,75 @@
import { RiLoopLeftLine } from '@remixicon/react'
import ConditionItem from './condition-item'
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
} & Omit<MetadataShape, 'handleAddCondition'>
const ConditionList = ({
disabled,
metadataList = [],
metadataFilteringConditions = {
conditions: [],
logical_operator: LogicalOperator.and,
},
handleRemoveCondition,
handleToggleConditionLogicalOperator,
handleUpdateCondition,
availableStringVars,
availableStringNodesWithParent,
availableNumberVars,
availableNumberNodesWithParent,
isCommonVariable,
availableCommonNumberVars,
availableCommonStringVars,
}: ConditionListProps) => {
const { conditions, logical_operator } = metadataFilteringConditions
return (
<div className={cn('relative')}>
{
conditions.length > 1 && (
<div className={cn(
'absolute top-0 bottom-0 left-0 w-[44px]',
)}>
<div className='absolute top-4 bottom-4 right-1 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>
)
}
<div className={cn(conditions.length > 1 && 'pl-[44px]')}>
{
conditions.map(condition => (
<ConditionItem
key={`${condition.id}`}
disabled={disabled}
condition={condition}
onUpdateCondition={handleUpdateCondition}
onRemoveCondition={handleRemoveCondition}
metadataList={metadataList}
availableStringVars={availableStringVars}
availableStringNodesWithParent={availableStringNodesWithParent}
availableNumberVars={availableNumberVars}
availableNumberNodesWithParent={availableNumberNodesWithParent}
isCommonVariable={isCommonVariable}
availableCommonStringVars={availableCommonStringVars}
availableCommonNumberVars={availableCommonNumberVars}
/>
))
}
</div>
</div>
)
}
export default ConditionList

View File

@@ -0,0 +1,65 @@
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.is,
ComparisonOperator.isNot,
ComparisonOperator.contains,
ComparisonOperator.notContains,
ComparisonOperator.startWith,
ComparisonOperator.endWith,
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.before,
ComparisonOperator.after,
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)
}
export const VARIABLE_REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
export const COMMON_VARIABLE_REGEX = /\{\{([a-zA-Z0-9_-]{1,50})\}\}/gi

View File

@@ -0,0 +1,101 @@
import {
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
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'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
type MetadataFilterProps = {
metadataFilterMode?: MetadataFilteringModeEnum
handleMetadataFilterModeChange: (mode: MetadataFilteringModeEnum) => void
} & MetadataShape
const MetadataFilter = ({
metadataFilterMode = MetadataFilteringModeEnum.disabled,
handleMetadataFilterModeChange,
metadataModelConfig,
handleMetadataModelChange,
handleMetadataCompletionParamsChange,
...restProps
}: MetadataFilterProps) => {
const { t } = useTranslation()
const [collapsed, setCollapsed] = useState(true)
const handleMetadataFilterModeChangeWrapped = useCallback((mode: MetadataFilteringModeEnum) => {
if (mode === MetadataFilteringModeEnum.automatic)
setCollapsed(false)
handleMetadataFilterModeChange(mode)
}, [handleMetadataFilterModeChange])
return (
<Collapse
disabled={metadataFilterMode === MetadataFilteringModeEnum.disabled || metadataFilterMode === MetadataFilteringModeEnum.manual}
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'>
{t('workflow.nodes.knowledgeRetrieval.metadata.title')}
</div>
<Tooltip
popupContent={(
<div className='w-[200px]'>
{t('workflow.nodes.knowledgeRetrieval.metadata.tip')}
</div>
)}
/>
</div>
<div className='flex items-center'>
<MetadataFilterSelector
value={metadataFilterMode}
onSelect={handleMetadataFilterModeChangeWrapped}
/>
{
metadataFilterMode === MetadataFilteringModeEnum.manual && (
<div className='ml-1'>
<MetadataTrigger {...restProps} />
</div>
)
}
</div>
</div>
}
>
<>
{
metadataFilterMode === MetadataFilteringModeEnum.automatic && (
<>
<div className='px-4 body-xs-regular text-text-tertiary'>
{t('workflow.nodes.knowledgeRetrieval.metadata.options.automatic.desc')}
</div>
<div className='mt-1 px-4'>
<ModelParameterModal
popupClassName='!w-[387px]'
isInWorkflow
isAdvancedMode={true}
mode={metadataModelConfig?.mode || 'chat'}
provider={metadataModelConfig?.provider || ''}
completionParams={metadataModelConfig?.completion_params || { temperature: 0.7 }}
modelId={metadataModelConfig?.name || ''}
setModel={handleMetadataModelChange || (() => {})}
onCompletionParamsChange={handleMetadataCompletionParamsChange || (() => {})}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
/>
</div>
</>
)
}
</>
</Collapse>
)
}
export default MetadataFilter

View File

@@ -0,0 +1,106 @@
import { 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'
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
type MetadataFilterSelectorProps = {
value?: MetadataFilteringModeEnum
onSelect: (value: MetadataFilteringModeEnum) => void
}
const MetadataFilterSelector = ({
value = MetadataFilteringModeEnum.disabled,
onSelect,
}: MetadataFilterSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const options = [
{
key: MetadataFilteringModeEnum.disabled,
value: t('workflow.nodes.knowledgeRetrieval.metadata.options.disabled.title'),
desc: t('workflow.nodes.knowledgeRetrieval.metadata.options.disabled.subTitle'),
},
{
key: MetadataFilteringModeEnum.automatic,
value: t('workflow.nodes.knowledgeRetrieval.metadata.options.automatic.title'),
desc: t('workflow.nodes.knowledgeRetrieval.metadata.options.automatic.subTitle'),
},
{
key: MetadataFilteringModeEnum.manual,
value: t('workflow.nodes.knowledgeRetrieval.metadata.options.manual.title'),
desc: t('workflow.nodes.knowledgeRetrieval.metadata.options.manual.subTitle'),
},
]
const selectedOption = options.find(option => option.key === value)!
return (
<PortalToFollowElem
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger
onClick={(e) => {
e.stopPropagation()
setOpen(!open)
}}
asChild
>
<Button
variant='secondary'
size='small'
>
{selectedOption.value}
<RiArrowDownSLine className='w-3.5 h-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='p-1 w-[280px] 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,39 @@
import { memo } from 'react'
import {
RiHashtag,
RiTextSnippet,
RiTimeLine,
} from '@remixicon/react'
import { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import cn from '@/utils/classnames'
type MetadataIconProps = {
type?: MetadataFilteringVariableType
className?: string
}
const MetadataIcon = ({
type,
className,
}: MetadataIconProps) => {
return (
<>
{
type === MetadataFilteringVariableType.string && (
<RiTextSnippet className={cn('w-3.5 h-3.5', className)} />
)
}
{
type === MetadataFilteringVariableType.number && (
<RiHashtag className={cn('w-3.5 h-3.5', className)} />
)
}
{
type === MetadataFilteringVariableType.time && (
<RiTimeLine className={cn('w-3.5 h-3.5', className)} />
)
}
</>
)
}
export default memo(MetadataIcon)

View File

@@ -0,0 +1,51 @@
import { useTranslation } from 'react-i18next'
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,
metadataList,
onCancel,
handleAddCondition,
...restProps
}: MetadataPanelProps) => {
const { t } = useTranslation()
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'>
{t('workflow.nodes.knowledgeRetrieval.metadata.panel.title')}
</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'>
<div className='pb-2'>
<ConditionList
metadataList={metadataList}
metadataFilteringConditions={metadataFilteringConditions}
{...restProps}
/>
</div>
<AddCondition
metadataList={metadataList}
handleAddCondition={handleAddCondition}
/>
</div>
</div>
</div>
)
}
export default MetadataPanel

View File

@@ -0,0 +1,69 @@
import {
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
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,
metadataList = [],
handleRemoveCondition,
selectedDatasetsLoaded,
...restProps
}: MetadataShape) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const conditions = metadataFilteringConditions?.conditions || []
useEffect(() => {
if (selectedDatasetsLoaded) {
conditions.forEach((condition) => {
if (!metadataList.find(metadata => metadata.name === condition.name))
handleRemoveCondition(condition.id)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metadataList, handleRemoveCondition, selectedDatasetsLoaded])
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' />
{t('workflow.nodes.knowledgeRetrieval.metadata.panel.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 || 0}
</div>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<MetadataPanel
metadataFilteringConditions={metadataFilteringConditions}
onCancel={() => setOpen(false)}
metadataList={metadataList}
handleRemoveCondition={handleRemoveCondition}
{...restProps}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default MetadataTrigger