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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 4775 additions and 101 deletions

View File

@ -1543,6 +1543,255 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata'
method='POST'
title='Create a Knowledge Metadata'
name='#create_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>type</code> (string) Metadata type, required
- <code>name</code> (string) Metadata name, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"type": "string", "name": "test"}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "abc",
"type": "string",
"name": "test",
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/{metadata_id}'
method='PATCH'
title='Update a Knowledge Metadata'
name='#update_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='metadata_id' type='string' key='metadata_id'>
Metadata ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>name</code> (string) Metadata name, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata/{metadata_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/{metadata_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"name": "test"}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "abc",
"type": "string",
"name": "test",
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/{metadata_id}'
method='DELETE'
title='Delete a Knowledge Metadata'
name='#delete_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='metadata_id' type='string' key='metadata_id'>
Metadata ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/{dataset_id}/metadata/{metadata_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/{metadata_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/built-in/{action}'
method='POST'
title='Disable Or Enable Built-in Metadata'
name='#toggle_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='action' type='string' key='action'>
disable/enable
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata/built-in/{action}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/built-in/{action}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/metadata'
method='POST'
title='Update Documents Metadata'
name='#update_documents_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='operation_data' type='object list' key='segments'>
- <code>document_id</code> (string) Document ID
- <code>metadata_list</code> (list) Metadata list
- <code>id</code> (string) Metadata ID
- <code>value</code> (string) Metadata value
- <code>name</code> (string) Metadata name
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/metadata"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/metadata' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"operation_data": [{"document_id": "document_id", "metadata_list": [{"id": "id", "value": "value", "name": "name"}]}]}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata'
method='GET'
title='Get Knowledge Metadata List'
name='#dataset_metadata_list'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/metadata"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/metadata' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"doc_metadata": [
{
"id": "",
"name": "name",
"type": "string",
"use_count": 0,
},
...
],
"built_in_field_enabled": true
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Row>
<Col>
### Error message

View File

@ -1547,6 +1547,254 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata'
method='POST'
title='新增元数据'
name='#create_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>type</code> (string) 元数据类型,必填
- <code>name</code> (string) 元数据名称,必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"type": "string", "name": "test"}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "abc",
"type": "string",
"name": "test",
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/{metadata_id}'
method='PATCH'
title='更新元数据'
name='#update_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='metadata_id' type='string' key='metadata_id'>
元数据 ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>name</code> (string) 元数据名称,必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata/{metadata_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/{metadata_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"name": "test"}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "abc",
"type": "string",
"name": "test",
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/{metadata_id}'
method='DELETE'
title='删除元数据'
name='#delete_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='metadata_id' type='string' key='metadata_id'>
元数据 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/{dataset_id}/metadata/{metadata_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/{metadata_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata/built-in/{action}'
method='POST'
title='启用/禁用内置元数据'
name='#toggle_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='action' type='string' key='action'>
disable/enable
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/metadata/built-in/{action}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/metadata/built-in/{action}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/metadata'
method='POST'
title='更新文档元数据'
name='#update_documents_metadata'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='operation_data' type='object list' key='segments'>
- <code>document_id</code> (string) 文档 ID
- <code>metadata_list</code> (list) 元数据列表
- <code>id</code> (string) 元数据 ID
- <code>type</code> (string) 元数据类型
- <code>name</code> (string) 元数据名称
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/metadata"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/metadata' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"operation_data": [{"document_id": "document_id", "metadata_list": [{"id": "id", "value": "value", "name": "name"}]}]}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/metadata'
method='GET'
title='查询知识库元数据列表'
name='#dataset_metadata_list'
/>
<Row>
<Col>
### Query
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/metadata"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/metadata' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"doc_metadata": [
{
"id": "",
"name": "name",
"type": "string",
"use_count": 0,
},
...
],
"built_in_field_enabled": true
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />

View File

@ -1,9 +1,11 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { intersectionBy } from 'lodash-es'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { v4 as uuid4 } from 'uuid'
import { useFormattingChangedDispatcher } from '../debug/hooks'
import FeaturePanel from '../base/feature-panel'
import OperationBtn from '../base/operation-btn'
@ -21,6 +23,19 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { hasEditPermissionForDataset } from '@/utils/permission'
import MetadataFilter from '@/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter'
import type {
HandleAddCondition,
HandleRemoveCondition,
HandleToggleConditionLogicalOperator,
HandleUpdateCondition,
MetadataFilteringModeEnum,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import {
ComparisonOperator,
LogicalOperator,
MetadataFilteringVariableType,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
const DatasetConfig: FC = () => {
const { t } = useTranslation()
@ -34,6 +49,7 @@ const DatasetConfig: FC = () => {
showSelectDataSet,
isAgent,
datasetConfigs,
datasetConfigsRef,
setDatasetConfigs,
setRerankSettingModalOpen,
} = useContext(ConfigContext)
@ -115,6 +131,98 @@ const DatasetConfig: FC = () => {
})
}, [dataSet, userProfile?.id])
const metadataList = useMemo(() => {
return intersectionBy(...formattedDataset.filter((dataset) => {
return !!dataset.doc_metadata
}).map((dataset) => {
return dataset.doc_metadata!
}), 'name')
}, [formattedDataset])
const handleMetadataFilterModeChange = useCallback((newMode: MetadataFilteringModeEnum) => {
setDatasetConfigs(produce(datasetConfigsRef.current!, (draft) => {
draft.metadata_filtering_mode = newMode
}))
}, [setDatasetConfigs, datasetConfigsRef])
const handleAddCondition = useCallback<HandleAddCondition>(({ name, type }) => {
let operator: ComparisonOperator = ComparisonOperator.is
if (type === MetadataFilteringVariableType.number)
operator = ComparisonOperator.equal
const newCondition = {
id: uuid4(),
name,
comparison_operator: operator,
}
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
if (draft.metadata_filtering_conditions) {
draft.metadata_filtering_conditions.conditions.push(newCondition)
}
else {
draft.metadata_filtering_conditions = {
logical_operator: LogicalOperator.and,
conditions: [newCondition],
}
}
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
const handleRemoveCondition = useCallback<HandleRemoveCondition>((id) => {
const conditions = datasetConfigsRef.current!.metadata_filtering_conditions?.conditions || []
const index = conditions.findIndex(c => c.id === id)
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
if (index > -1)
draft.metadata_filtering_conditions?.conditions.splice(index, 1)
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
const handleUpdateCondition = useCallback<HandleUpdateCondition>((id, newCondition) => {
console.log(newCondition, 'newCondition')
const conditions = datasetConfigsRef.current!.metadata_filtering_conditions?.conditions || []
const index = conditions.findIndex(c => c.id === id)
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
if (index > -1)
draft.metadata_filtering_conditions!.conditions[index] = newCondition
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
const oldLogicalOperator = datasetConfigsRef.current!.metadata_filtering_conditions?.logical_operator
const newLogicalOperator = oldLogicalOperator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
draft.metadata_filtering_conditions!.logical_operator = newLogicalOperator
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
const handleMetadataModelChange = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
draft.metadata_model_config = {
provider: model.provider,
name: model.modelId,
mode: model.mode || 'chat',
completion_params: draft.metadata_model_config?.completion_params || { temperature: 0.7 },
}
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
const handleMetadataCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
draft.metadata_model_config = {
...draft.metadata_model_config!,
completion_params: newParams,
}
})
setDatasetConfigs(newInputs)
}, [setDatasetConfigs, datasetConfigsRef])
return (
<FeaturePanel
className='mt-2'
@ -148,6 +256,26 @@ const DatasetConfig: FC = () => {
</div>
)}
<div className='py-2 border-t border-t-divider-subtle'>
<MetadataFilter
metadataList={metadataList}
selectedDatasetsLoaded
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
handleAddCondition={handleAddCondition}
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
handleRemoveCondition={handleRemoveCondition}
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
handleUpdateCondition={handleUpdateCondition}
metadataModelConfig={datasetConfigs.metadata_model_config}
handleMetadataModelChange={handleMetadataModelChange}
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
isCommonVariable
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string)}
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
/>
</div>
{mode === AppType.completion && dataSet.length > 0 && (
<ContextVar
value={selectedContextVar?.key}

View File

@ -191,7 +191,6 @@ const Configuration: FC = () => {
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
})
const isAgent = mode === 'agent-chat'
const isOpenAI = modelConfig.provider === 'langgenius/openai/openai'
@ -200,7 +199,7 @@ const Configuration: FC = () => {
useEffect(() => {
}, [])
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>({
const [datasetConfigs, doSetDatasetConfigs] = useState<DatasetConfigs>({
retrieval_model: RETRIEVE_TYPE.multiWay,
reranking_model: {
reranking_provider_name: '',
@ -213,6 +212,11 @@ const Configuration: FC = () => {
datasets: [],
},
})
const datasetConfigsRef = useRef(datasetConfigs)
const setDatasetConfigs = useCallback((newDatasetConfigs: DatasetConfigs) => {
doSetDatasetConfigs(newDatasetConfigs)
datasetConfigsRef.current = newDatasetConfigs
}, [])
const setModelConfig = (newModelConfig: ModelConfig) => {
doSetModelConfig(newModelConfig)
@ -292,6 +296,7 @@ const Configuration: FC = () => {
})
setDatasetConfigs({
...datasetConfigsRef.current,
...retrievalConfig,
reranking_model: {
reranking_provider_name: retrievalConfig?.reranking_model?.provider || '',
@ -884,6 +889,7 @@ const Configuration: FC = () => {
dataSets,
setDataSets,
datasetConfigs,
datasetConfigsRef,
setDatasetConfigs,
hasSetContextVar,
isShowVisionConfig,

View File

@ -34,6 +34,8 @@ const DatePicker = ({
placeholder,
needTimePicker = true,
renderTrigger,
triggerWrapClassName,
popupZIndexClassname = 'z-[11]',
}: DatePickerProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
@ -127,7 +129,9 @@ const DatePicker = ({
}
const handleConfirmDate = () => {
onChange(selectedDate)
// debugger
console.log(selectedDate, selectedDate?.tz(timezone))
onChange(selectedDate ? selectedDate.tz(timezone) : undefined)
setIsOpen(false)
}
@ -200,7 +204,7 @@ const DatePicker = ({
onOpenChange={setIsOpen}
placement='bottom-end'
>
<PortalToFollowElemTrigger>
<PortalToFollowElemTrigger className={triggerWrapClassName}>
{renderTrigger ? (renderTrigger({
value,
selectedDate,
@ -234,7 +238,7 @@ const DatePicker = ({
</div>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<PortalToFollowElemContent className={popupZIndexClassname}>
<div className='w-[252px] mt-1 bg-components-panel-bg rounded-xl shadow-lg shadow-shadow-shadow-5 border-[0.5px] border-components-panel-border'>
{/* Header */}
{view === ViewType.date ? (

View File

@ -11,7 +11,7 @@ export enum Period {
PM = 'PM',
}
type TriggerProps = {
export type TriggerProps = {
value: Dayjs | undefined
selectedDate: Dayjs | undefined
isOpen: boolean
@ -26,7 +26,9 @@ export type DatePickerProps = {
needTimePicker?: boolean
onChange: (date: Dayjs | undefined) => void
onClear: () => void
triggerWrapClassName?: string
renderTrigger?: (props: TriggerProps) => React.ReactNode
popupZIndexClassname?: string
}
export type DatePickerHeaderProps = {

View File

@ -53,15 +53,17 @@ export default function Drawer({
/>
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
<>
<div className='flex justify-between'>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-text-primary"
>
{title}
</Dialog.Title>}
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
{showClose && <Dialog.Title className="flex items-center mb-4 cursor-pointer" as="div">
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
</Dialog.Title>}
</div>
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
{children}
</>

View File

@ -13,10 +13,13 @@ export type InputNumberProps = {
min?: number
defaultValue?: number
disabled?: boolean
wrapClassName?: string
controlWrapClassName?: string
controlClassName?: string
} & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
export const InputNumber: FC<InputNumberProps> = (props) => {
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, disabled, ...rest } = props
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, disabled, ...rest } = props
const isValidValue = (v: number) => {
if (max && v > max)
@ -51,7 +54,7 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
onChange(newValue)
}
return <div className='flex'>
return <div className={classNames('flex', wrapClassName)}>
<Input {...rest}
// disable default controller
type='text'
@ -77,16 +80,14 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
<div className={classNames(
'flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs',
disabled && 'opacity-50 cursor-not-allowed',
)}>
<button
onClick={inc}
disabled={disabled}
className={classNames(
controlWrapClassName)}
>
<button onClick={inc} disabled={disabled} className={classNames(
size === 'sm' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
disabled && 'cursor-not-allowed hover:bg-transparent',
)}
>
controlClassName,
)}>
<RiArrowUpSLine className='size-3' />
</button>
<button
@ -96,8 +97,8 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
size === 'sm' ? 'pb-1' : 'pb-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
disabled && 'cursor-not-allowed hover:bg-transparent',
)}
>
controlClassName,
)}>
<RiArrowDownSLine className='size-3' />
</button>
</div>

View File

@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import Button from '../button'
import { RiCloseLine } from '@remixicon/react'
type Props = {
title: string
className?: string
beforeHeader?: React.ReactNode
onClose: () => void
hideCloseBtn?: boolean
onConfirm: () => void
children: React.ReactNode
}
const ModalLikeWrap: FC<Props> = ({
title,
className,
beforeHeader,
children,
onClose,
hideCloseBtn,
onConfirm,
}) => {
const { t } = useTranslation()
return (
<div className={cn('w-[320px] px-3 pt-3.5 pb-4 bg-components-panel-bg shadow-xl rounded-2xl border-[0.5px] border-components-panel-border', className)}>
{beforeHeader || null}
<div className='mb-1 flex h-6 items-center justify-between'>
<div className='system-xl-semibold text-text-primary'>{title}</div>
{!hideCloseBtn && (
<div
className='p-1.5 text-text-tertiary cursor-pointer'
onClick={onClose}
>
<RiCloseLine className='size-4' />
</div>
)}
</div>
<div className='mt-2'>{children}</div>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
onClick={onConfirm}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
)
}
export default React.memo(ModalLikeWrap)

View File

@ -9,8 +9,8 @@ type Status = 'success' | 'error' | 'warning' | 'info'
type Props = {
type?: Status
description: string
actionText: string
onAction: () => void
actionText?: string
onAction?: () => void
disabled?: boolean
}
@ -47,7 +47,8 @@ const StatusAction: FC<Props> = ({
const { Icon, color } = getIcon(type)
return (
<div className='relative flex items-center h-[34px] rounded-lg pl-2 pr-3 border border-components-panel-border bg-components-panel-bg-blur shadow-xs'>
<div className={`absolute inset-0 opacity-40 rounded-lg ${(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
<div className={
`absolute inset-0 opacity-40 rounded-lg ${(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
@ -56,8 +57,12 @@ const StatusAction: FC<Props> = ({
<div className='relative z-10 flex h-full items-center space-x-2'>
<Icon className={cn('w-4 h-4', color)} />
<div className='text-[13px] font-normal text-text-secondary'>{description}</div>
{onAction && (
<>
<Divider type='vertical' className='!h-4' />
<div onClick={onAction} className={cn('text-text-accent font-semibold text-[13px] cursor-pointer', disabled && 'text-text-disabled cursor-not-allowed')}>{actionText}</div>
</>
)}
</div>
</div>
)

View File

@ -1,5 +1,5 @@
import React, { type FC } from 'react'
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine } from '@remixicon/react'
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine, RiDraftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import Divider from '@/app/components/base/divider'
@ -14,6 +14,7 @@ type IBatchActionProps = {
onBatchDisable: () => void
onBatchDelete: () => Promise<void>
onArchive?: () => void
onEditMetadata?: () => void
onCancel: () => void
}
@ -24,6 +25,7 @@ const BatchAction: FC<IBatchActionProps> = ({
onBatchDisable,
onArchive,
onBatchDelete,
onEditMetadata,
onCancel,
}) => {
const { t } = useTranslation()
@ -62,6 +64,15 @@ const BatchAction: FC<IBatchActionProps> = ({
{t(`${i18nPrefix}.disable`)}
</button>
</div>
{onEditMetadata && (
<div className='flex items-center gap-x-0.5 px-3 py-2'>
<RiDraftLine className='w-4 h-4 text-components-button-ghost-text' />
<button type='button' className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onEditMetadata}>
{t('dataset.metadata.metadata')}
</button>
</div>
)}
{onArchive && (
<div className='flex items-center gap-x-0.5 px-3 py-2'>
<RiArchive2Line className='w-4 h-4 text-components-button-ghost-text' />

View File

@ -9,7 +9,7 @@ import { OperationAction, StatusItem } from '../list'
import DocumentPicker from '../../common/document-picker'
import Completed from './completed'
import Embedding from './embedding'
import Metadata from './metadata'
import Metadata from '@/app/components/datasets/metadata/metadata-document'
import SegmentAdd, { ProcessStatus } from './segment-add'
import BatchModal from './batch-modal'
import style from './style.module.css'
@ -281,9 +281,10 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
}
<FloatRightContainer showClose isOpen={showMetadata} onClose={() => setShowMetadata(false)} isMobile={isMobile} panelClassname='!justify-start' footer={null}>
<Metadata
className='mr-2 mt-3'
datasetId={datasetId}
documentId={documentId}
docDetail={{ ...documentDetail, ...documentMetadata, doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type } as any}
loading={isMetadataLoading}
onUpdate={metadataMutate}
/>
</FloatRightContainer>
</div>

View File

@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'
import { useDebounce, useDebounceFn } from 'ahooks'
import { groupBy } from 'lodash-es'
import { PlusIcon } from '@heroicons/react/24/solid'
import { RiExternalLinkLine } from '@remixicon/react'
import { RiDraftLine, RiExternalLinkLine } from '@remixicon/react'
import AutoDisabledDocument from '../common/document-status-with-action/auto-disabled-document'
import List from './list'
import s from './style.module.css'
@ -26,6 +26,9 @@ import cn from '@/utils/classnames'
import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document'
import { useInvalid } from '@/service/use-base'
import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment'
import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata'
import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer'
import StatusWithAction from '../common/document-status-with-action/status-with-action'
const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
@ -231,6 +234,23 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
handleSearch()
}
const {
isShowEditModal: isShowEditMetadataModal,
showEditModal: showEditMetadataModal,
hideEditModal: hideEditMetadataModal,
datasetMetaData,
handleAddMetaData,
handleRename,
handleDeleteMetaData,
builtInEnabled,
setBuiltInEnabled,
builtInMetaData,
} = useEditDocumentMetadata({
datasetId,
dataset,
onUpdateDocList: invalidDocumentList,
})
return (
<div className='flex flex-col h-full overflow-y-auto'>
<div className='flex flex-col justify-center gap-1 px-6 pt-4'>
@ -259,6 +279,25 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
<div className='flex gap-2 justify-center items-center !h-8'>
{!isFreePlan && <AutoDisabledDocument datasetId={datasetId} />}
<IndexFailed datasetId={datasetId} />
{!embeddingAvailable && <StatusWithAction type='warning' description={t('dataset.embeddingModelNotAvailable')} />}
{embeddingAvailable && (
<Button variant='secondary' className='shrink-0' onClick={showEditMetadataModal}>
<RiDraftLine className='size-4 mr-1' />
{t('dataset.metadata.metadata')}
</Button>
)}
{isShowEditMetadataModal && (
<DatasetMetadataDrawer
userMetadata={datasetMetaData || []}
onClose={hideEditMetadataModal}
onAdd={handleAddMetaData}
onRename={handleRename}
onRemove={handleDeleteMetaData}
builtInMetadata={builtInMetaData || []}
isBuiltInEnabled={!!builtInEnabled}
onIsBuiltInEnabledChange={setBuiltInEnabled}
/>
)}
{embeddingAvailable && (
<Button variant='primary' onClick={routeToDocCreate} className='shrink-0'>
<PlusIcon className={cn('h-4 w-4 mr-2 stroke-current')} />
@ -286,6 +325,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
current: currPage,
onChange: setCurrPage,
}}
onManageMetadata={showEditMetadataModal}
/>
: <EmptyElement canAdd={embeddingAvailable} onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} />
}

View File

@ -45,6 +45,8 @@ import Pagination from '@/app/components/base/pagination'
import Checkbox from '@/app/components/base/checkbox'
import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document'
import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
export const useIndexStatus = () => {
const { t } = useTranslation()
@ -107,7 +109,8 @@ export const StatusItem: FC<{
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onUpdate?.(operationName)
onUpdate?.()
// onUpdate?.(operationName)
}
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
}
@ -401,6 +404,7 @@ type IDocumentListProps = {
datasetId: string
pagination: PaginationProps
onUpdate: () => void
onManageMetadata: () => void
}
/**
@ -414,6 +418,7 @@ const DocumentList: FC<IDocumentListProps> = ({
datasetId,
pagination,
onUpdate,
onManageMetadata,
}) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
@ -424,6 +429,17 @@ const DocumentList: FC<IDocumentListProps> = ({
const isQAMode = chunkingMode === ChunkingMode.qa
const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
const [enableSort, setEnableSort] = useState(true)
const {
isShowEditModal,
showEditModal,
hideEditModal,
originalList,
handleSave,
} = useBatchEditDocumentMetadata({
datasetId,
docList: documents.filter(item => selectedIds.includes(item.id)),
onUpdate,
})
useEffect(() => {
setLocalDocs(documents)
@ -501,18 +517,20 @@ const DocumentList: FC<IDocumentListProps> = ({
return (
<div className='flex flex-col relative w-full h-full'>
<div className='grow overflow-x-auto'>
<div className='relative grow overflow-x-auto'>
<table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}>
<thead className="h-8 leading-8 border-b border-divider-subtle text-text-tertiary font-medium text-xs uppercase">
<tr>
<td className='w-12'>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
{embeddingAvailable && (
<Checkbox
className='shrink-0 mr-2'
checked={isAllSelected}
mixed={!isAllSelected && isSomeSelected}
onCheck={onSelectedAll}
/>
)}
#
</div>
</td>
@ -625,6 +643,7 @@ const DocumentList: FC<IDocumentListProps> = ({
onBatchEnable={handleAction(DocumentActionType.enable)}
onBatchDisable={handleAction(DocumentActionType.disable)}
onBatchDelete={handleAction(DocumentActionType.delete)}
onEditMetadata={showEditModal}
onCancel={() => {
onSelectedIdChange([])
}}
@ -647,6 +666,20 @@ const DocumentList: FC<IDocumentListProps> = ({
onSaved={handleRenamed}
/>
)}
{isShowEditModal && (
<EditMetadataBatchModal
datasetId={datasetId}
documentNum={selectedIds.length}
list={originalList}
onSave={handleSave}
onHide={hideEditModal}
onShowManage={() => {
hideEditModal()
onManageMetadata()
}}
/>
)}
</div>
)
}

View File

@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Button from '../../base/button'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
onClick?: () => void
}
const AddedMetadataButton: FC<Props> = ({
className,
onClick,
}) => {
const { t } = useTranslation()
return (
<Button
className={cn('w-full flex items-center', className)}
size='small'
variant='tertiary'
onClick={onClick}
>
<RiAddLine className='mr-1 size-3.5' />
<div>{t('dataset.metadata.addMetadata')}</div>
</Button>
)
}
export default React.memo(AddedMetadataButton)

View File

@ -0,0 +1,76 @@
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'
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
import useTimestamp from '@/hooks/use-timestamp'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
value?: number
onChange: (date: number | null) => void
}
const WrappedDatePicker = ({
className,
value,
onChange,
}: Props) => {
const { t } = useTranslation()
// const { userProfile: { timezone } } = useAppContext()
const { formatTime: formatTimestamp } = useTimestamp()
const handleDateChange = useCallback((date?: dayjs.Dayjs) => {
if (date)
onChange(date.unix())
else
onChange(null)
}, [onChange])
const renderTrigger = useCallback(({
handleClickTrigger,
}: TriggerProps) => {
return (
<div onClick={handleClickTrigger} className={cn('group flex items-center rounded-md bg-components-input-bg-normal', className)}>
<div
className={cn(
'grow',
value ? 'text-text-secondary' : 'text-text-tertiary',
)}
>
{value ? formatTimestamp(value, t('datasetDocuments.metadata.dateTimeFormat')) : t('dataset.metadata.chooseTime')}
</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>
)
}, [className, value, formatTimestamp, t, handleDateChange])
return (
<DatePicker
value={dayjs(value ? value * 1000 : Date.now())}
onChange={handleDateChange}
onClear={handleDateChange}
renderTrigger={renderTrigger}
triggerWrapClassName='w-full'
popupZIndexClassname='z-[1000]'
/>
)
}
export default WrappedDatePicker

View File

@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { MetadataItemWithEdit } from '../types'
import cn from '@/utils/classnames'
import Label from './label'
import InputCombined from './input-combined'
import { RiIndeterminateCircleLine } from '@remixicon/react'
type Props = {
className?: string
payload: MetadataItemWithEdit
onChange: (value: MetadataItemWithEdit) => void
onRemove: () => void
}
const AddRow: FC<Props> = ({
className,
payload,
onChange,
onRemove,
}) => {
return (
<div className={cn('flex h-6 items-center space-x-0.5', className)}>
<Label text={payload.name} />
<InputCombined
type={payload.type}
value={payload.value}
onChange={value => onChange({ ...payload, value })}
/>
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
)
}
onClick={onRemove}
>
<RiIndeterminateCircleLine className='size-4' />
</div>
</div>
)
}
export default React.memo(AddRow)

View File

@ -0,0 +1,56 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { type MetadataItemWithEdit, UpdateType } from '../types'
import Label from './label'
import { RiDeleteBinLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import InputHasSetMultipleValue from './input-has-set-multiple-value'
import InputCombined from './input-combined'
import EditedBeacon from './edited-beacon'
type Props = {
payload: MetadataItemWithEdit
onChange: (payload: MetadataItemWithEdit) => void
onRemove: (id: string) => void
onReset: (id: string) => void
}
const EditMetadatabatchItem: FC<Props> = ({
payload,
onChange,
onRemove,
onReset,
}) => {
const isUpdated = payload.isUpdated
const isDeleted = payload.updateType === UpdateType.delete
return (
<div className='flex h-6 items-center space-x-0.5'>
{isUpdated ? <EditedBeacon onReset={() => onReset(payload.id)} /> : <div className='shrink-0 size-4' />}
<Label text={payload.name} isDeleted={isDeleted} />
{payload.isMultipleValue
? <InputHasSetMultipleValue
onClear={() => onChange({ ...payload, value: null, isMultipleValue: false })}
readOnly={isDeleted}
/>
: <InputCombined
type={payload.type}
value={payload.value}
onChange={v => onChange({ ...payload, value: v as string })}
readOnly={isDeleted}
/>}
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
isDeleted && 'cursor-default bg-state-destructive-hover text-text-destructive')
}
onClick={() => onRemove(payload.id)}
>
<RiDeleteBinLine className='size-4' />
</div>
</div>
)
}
export default React.memo(EditMetadatabatchItem)

View File

@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React, { useRef } from 'react'
import { useHover } from 'ahooks'
import { RiResetLeftLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
type Props = {
onReset: () => void
}
const EditedBeacon: FC<Props> = ({
onReset,
}) => {
const { t } = useTranslation()
const ref = useRef(null)
const isHovering = useHover(ref)
return (
<div ref={ref} className='size-4 cursor-pointer'>
{isHovering ? (
<Tooltip popupContent={t('common.operation.reset')}>
<div className='flex justify-center items-center size-4 bg-text-accent-secondary rounded-full' onClick={onReset}>
<RiResetLeftLine className='size-[10px] text-text-primary-on-surface' />
</div>
</Tooltip>
) : (
<div className='flex items-center justify-center size-4'>
<div className='size-1 rounded-full bg-text-accent-secondary'></div>
</div>
)}
</div>
)
}
export default React.memo(EditedBeacon)

View File

@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { DataType } from '../types'
import Input from '@/app/components/base/input'
import { InputNumber } from '@/app/components/base/input-number'
import cn from '@/utils/classnames'
import Datepicker from '../base/date-picker'
type Props = {
className?: string
type: DataType
value: any
onChange: (value: any) => void
readOnly?: boolean
}
const InputCombined: FC<Props> = ({
className: configClassName,
type,
value,
onChange,
readOnly,
}) => {
const className = cn('grow p-0.5 h-6 text-xs')
if (type === DataType.time) {
return (
<Datepicker
className={className}
value={value}
onChange={onChange}
/>
)
}
if (type === DataType.number) {
return (
<div className='grow text-[0]'>
<InputNumber
className={cn(className, 'rounded-l-md')}
value={value}
onChange={onChange}
size='sm'
controlWrapClassName='overflow-hidden'
controlClassName='pt-0 pb-0'
readOnly={readOnly}
/>
</div>
)
}
return (
<Input
wrapperClassName={configClassName}
className={cn(className, 'rounded-md')}
value={value}
onChange={e => onChange(e.target.value)}
readOnly={readOnly}
/>
)
}
export default React.memo(InputCombined)

View File

@ -0,0 +1,34 @@
'use client'
import { RiCloseLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
type Props = {
onClear: () => void
readOnly?: boolean
}
const InputHasSetMultipleValue: FC<Props> = ({
onClear,
readOnly,
}) => {
const { t } = useTranslation()
return (
<div className='grow h-6 p-0.5 rounded-md bg-components-input-bg-normal text-[0]'>
<div className={cn('inline-flex rounded-[5px] items-center h-5 pl-1.5 pr-0.5 bg-components-badge-white-to-dark border-[0.5px] border-components-panel-border shadow-xs space-x-0.5', readOnly && 'pr-1.5')}>
<div className='system-xs-regular text-text-secondary'>{t('dataset.metadata.batchEditMetadata.multipleValue')}</div>
{!readOnly && (
<div className='p-px rounded-[4px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer'>
<RiCloseLine
className='size-3.5 '
onClick={onClear}
/>
</div>
)}
</div>
</div>
)
}
export default React.memo(InputHasSetMultipleValue)

View File

@ -0,0 +1,27 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
isDeleted?: boolean,
className?: string,
text: string
}
const Label: FC<Props> = ({
isDeleted,
className,
text,
}) => {
return (
<div className={cn(
'shrink-0 w-[136px] system-xs-medium text-text-tertiary truncate',
isDeleted && 'line-through text-text-quaternary',
className,
)}>
{text}
</div>
)
}
export default React.memo(Label)

View File

@ -0,0 +1,189 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import Modal from '../../../base/modal'
import type { BuiltInMetadataItem, MetadataItemInBatchEdit } from '../types'
import { type MetadataItemWithEdit, UpdateType } from '../types'
import EditMetadataBatchItem from './edit-row'
import AddedMetadataItem from './add-row'
import Button from '../../../base/button'
import { useTranslation } from 'react-i18next'
import Checkbox from '../../../base/checkbox'
import Tooltip from '../../../base/tooltip'
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
import { RiQuestionLine } from '@remixicon/react'
import Divider from '@/app/components/base/divider'
import AddMetadataButton from '../add-metadata-button'
import produce from 'immer'
import useCheckMetadataName from '../hooks/use-check-metadata-name'
import Toast from '@/app/components/base/toast'
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
const i18nPrefix = 'dataset.metadata.batchEditMetadata'
type Props = {
datasetId: string,
documentNum: number
list: MetadataItemInBatchEdit[]
onSave: (editedList: MetadataItemInBatchEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean) => void
onHide: () => void
onShowManage: () => void
}
const EditMetadataBatchModal: FC<Props> = ({
datasetId,
documentNum,
list,
onSave,
onHide,
onShowManage,
}) => {
const { t } = useTranslation()
const [templeList, setTempleList] = useState<MetadataItemWithEdit[]>(list)
const handleTemplesChange = useCallback((payload: MetadataItemWithEdit) => {
const newTempleList = produce(templeList, (draft) => {
const index = draft.findIndex(i => i.id === payload.id)
if (index !== -1) {
draft[index] = payload
draft[index].isUpdated = true
draft[index].updateType = UpdateType.changeValue
}
},
)
setTempleList(newTempleList)
}, [templeList])
const handleTempleItemRemove = useCallback((id: string) => {
const newTempleList = produce(templeList, (draft) => {
const index = draft.findIndex(i => i.id === id)
if (index !== -1) {
draft[index].isUpdated = true
draft[index].updateType = UpdateType.delete
}
})
setTempleList(newTempleList)
}, [templeList])
const handleItemReset = useCallback((id: string) => {
const newTempleList = produce(templeList, (draft) => {
const index = draft.findIndex(i => i.id === id)
if (index !== -1) {
draft[index] = { ...list[index] }
draft[index].isUpdated = false
delete draft[index].updateType
}
})
setTempleList(newTempleList)
}, [list, templeList])
const { checkName } = useCheckMetadataName()
const { mutate: doAddMetaData } = useCreateMetaData(datasetId)
const handleAddMetaData = useCallback(async (payload: BuiltInMetadataItem) => {
const errorMsg = checkName(payload.name).errorMsg
if (errorMsg) {
Toast.notify({
message: errorMsg,
type: 'error',
})
return Promise.reject(new Error(errorMsg))
}
await doAddMetaData(payload)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}, [checkName, doAddMetaData, t])
const [addedList, setAddedList] = useState<MetadataItemWithEdit[]>([])
const handleAddedListChange = useCallback((payload: MetadataItemWithEdit) => {
const newAddedList = addedList.map(i => i.id === payload.id ? payload : i)
setAddedList(newAddedList)
}, [addedList])
const handleAddedItemRemove = useCallback((removeIndex: number) => {
return () => {
const newAddedList = addedList.filter((i, index) => index !== removeIndex)
setAddedList(newAddedList)
}
}, [addedList])
const [isApplyToAllSelectDocument, setIsApplyToAllSelectDocument] = useState(false)
const handleSave = useCallback(() => {
onSave(templeList.filter(item => item.updateType !== UpdateType.delete), addedList, isApplyToAllSelectDocument)
}, [templeList, addedList, isApplyToAllSelectDocument, onSave])
return (
<Modal
title={t(`${i18nPrefix}.editMetadata`)}
isShow
closable
onClose={onHide}
className='!max-w-[640px]'
>
<div className='mt-1 system-xs-medium text-text-accent'>{t(`${i18nPrefix}.editDocumentsNum`, { num: documentNum })}</div>
<div className='ml-[-16px] max-h-[305px] overflow-y-auto'>
<div className='mt-4 space-y-2'>
{templeList.map(item => (
<EditMetadataBatchItem
key={item.id}
payload={item}
onChange={handleTemplesChange}
onRemove={handleTempleItemRemove}
onReset={handleItemReset}
/>
))}
</div>
<div className='mt-4 pl-[18px]'>
<div className='flex items-center'>
<div className='mr-2 shrink-0 system-xs-medium-uppercase text-text-tertiary'>{t('dataset.metadata.createMetadata.title')}</div>
<Divider bgStyle='gradient' />
</div>
<div className='mt-2 space-y-2'>
{addedList.map((item, i) => (
<AddedMetadataItem
key={i}
payload={item}
onChange={handleAddedListChange}
onRemove={handleAddedItemRemove(i)}
/>
))}
</div>
<div className='mt-3'>
<SelectMetadataModal
datasetId={datasetId}
popupPlacement='top-start'
popupOffset={{ mainAxis: 4, crossAxis: 0 }}
trigger={
<AddMetadataButton />
}
onSave={handleAddMetaData}
onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])}
onManage={onShowManage}
/>
</div>
</div>
</div>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center select-none'>
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} />
<div className='ml-2 mr-1 system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.applyToAllSelectDocument`)}</div>
<Tooltip popupContent={
<div className='max-w-[240px]'>{t(`${i18nPrefix}.applyToAllSelectDocumentTip`)}</div>
} >
<div className='p-px cursor-pointer'>
<RiQuestionLine className='size-3.5 text-text-tertiary' />
</div>
</Tooltip>
</div>
<div className='flex items-center space-x-2'>
<Button
onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleSave}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(EditMetadataBatchModal)

View File

@ -0,0 +1,143 @@
import { useBoolean } from 'ahooks'
import { type MetadataBatchEditToServer, type MetadataItemInBatchEdit, type MetadataItemWithEdit, type MetadataItemWithValue, UpdateType } from '../types'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { useMemo } from 'react'
import { useBatchUpdateDocMetadata } from '@/service/knowledge/use-metadata'
import Toast from '@/app/components/base/toast'
import { t } from 'i18next'
type Props = {
datasetId: string
docList: SimpleDocumentDetail[]
onUpdate: () => void
}
const useBatchEditDocumentMetadata = ({
datasetId,
docList,
onUpdate,
}: Props) => {
const [isShowEditModal, {
setTrue: showEditModal,
setFalse: hideEditModal,
}] = useBoolean(false)
const metaDataList: MetadataItemWithValue[][] = (() => {
const res: MetadataItemWithValue[][] = []
docList.forEach((item) => {
if (item.doc_metadata) {
res.push(item.doc_metadata.filter(item => item.id !== 'built-in'))
return
}
res.push([])
})
return res
})()
// To check is key has multiple value
const originalList: MetadataItemInBatchEdit[] = useMemo(() => {
const idNameValue: Record<string, { value: string | number | null, isMultipleValue: boolean }> = {}
const res: MetadataItemInBatchEdit[] = []
metaDataList.forEach((metaData) => {
metaData.forEach((item) => {
if (idNameValue[item.id]?.isMultipleValue)
return
const itemInRes = res.find(i => i.id === item.id)
if (!idNameValue[item.id]) {
idNameValue[item.id] = {
value: item.value,
isMultipleValue: false,
}
}
if (itemInRes && itemInRes.value !== item.value) {
idNameValue[item.id].isMultipleValue = true
itemInRes.isMultipleValue = true
itemInRes.value = null
return
}
if (!itemInRes) {
res.push({
...item,
isMultipleValue: false,
})
}
})
})
return res
}, [metaDataList])
const formateToBackendList = (editedList: MetadataItemWithEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean) => {
const updatedList = editedList.filter((editedItem) => {
return editedItem.updateType === UpdateType.changeValue
})
const removedList = originalList.filter((originalItem) => {
const editedItem = editedList.find(i => i.id === originalItem.id)
if (!editedItem) // removed item
return true
return false
})
const res: MetadataBatchEditToServer = docList.map((item, i) => {
// the new metadata will override the old one
const oldMetadataList = metaDataList[i]
let newMetadataList: MetadataItemWithValue[] = [...oldMetadataList, ...addedList]
.filter((item) => {
return !removedList.find(removedItem => removedItem.id === item.id)
})
.map(item => ({
id: item.id,
name: item.name,
type: item.type,
value: item.value,
}))
if (isApplyToAllSelectDocument) {
// add missing metadata item
updatedList.forEach((editedItem) => {
if (!newMetadataList.find(i => i.id === editedItem.id) && !editedItem.isMultipleValue)
newMetadataList.push(editedItem)
})
}
newMetadataList = newMetadataList.map((item) => {
const editedItem = updatedList.find(i => i.id === item.id)
if (editedItem)
return editedItem
return item
})
return {
document_id: item.id,
metadata_list: newMetadataList,
}
})
return res
}
const { mutateAsync } = useBatchUpdateDocMetadata()
const handleSave = async (editedList: MetadataItemInBatchEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean) => {
const backendList = formateToBackendList(editedList, addedList, isApplyToAllSelectDocument)
await mutateAsync({
dataset_id: datasetId,
metadata_list: backendList,
})
onUpdate()
hideEditModal()
Toast.notify({
type: 'success',
message: t('common.actionMsg.modifiedSuccessfully'),
})
}
return {
isShowEditModal,
showEditModal,
hideEditModal,
originalList,
handleSave,
}
}
export default useBatchEditDocumentMetadata

View File

@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'dataset.metadata.checkName'
const useCheckMetadataName = () => {
const { t } = useTranslation()
return {
checkName: (name: string) => {
if (!name) {
return {
errorMsg: t(`${i18nPrefix}.empty`),
}
}
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
return {
errorMsg: t(`${i18nPrefix}.invalid`),
}
}
return {
errorMsg: '',
}
},
}
}
export default useCheckMetadataName

View File

@ -0,0 +1,96 @@
import { useBoolean } from 'ahooks'
import { useBuiltInMetaDataFields, useCreateMetaData, useDatasetMetaData, useDeleteMetaData, useRenameMeta, useUpdateBuiltInStatus } from '@/service/knowledge/use-metadata'
import type { DataSet } from '@/models/datasets'
import { useCallback, useEffect, useState } from 'react'
import { type BuiltInMetadataItem, type MetadataItemWithValueLength, isShowManageMetadataLocalStorageKey } from '../types'
import useCheckMetadataName from './use-check-metadata-name'
import Toast from '@/app/components/base/toast'
import { useTranslation } from 'react-i18next'
const useEditDatasetMetadata = ({
datasetId,
// dataset,
onUpdateDocList,
}: {
datasetId: string,
dataset?: DataSet,
onUpdateDocList: () => void
}) => {
const { t } = useTranslation()
const [isShowEditModal, {
setTrue: showEditModal,
setFalse: hideEditModal,
}] = useBoolean(false)
useEffect(() => {
const isShowManageMetadata = localStorage.getItem(isShowManageMetadataLocalStorageKey)
if (isShowManageMetadata) {
showEditModal()
localStorage.removeItem(isShowManageMetadataLocalStorageKey)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
const { mutate: doAddMetaData } = useCreateMetaData(datasetId)
const { checkName } = useCheckMetadataName()
const handleAddMetaData = useCallback(async (payload: BuiltInMetadataItem) => {
const errorMsg = checkName(payload.name).errorMsg
if (errorMsg) {
Toast.notify({
message: errorMsg,
type: 'error',
})
return Promise.reject(new Error(errorMsg))
}
await doAddMetaData(payload)
}, [checkName, doAddMetaData])
const { mutate: doRenameMetaData } = useRenameMeta(datasetId)
const handleRename = useCallback(async (payload: MetadataItemWithValueLength) => {
const errorMsg = checkName(payload.name).errorMsg
if (errorMsg) {
Toast.notify({
message: errorMsg,
type: 'error',
})
return Promise.reject(new Error(errorMsg))
}
await doRenameMetaData(payload)
onUpdateDocList()
}, [checkName, doRenameMetaData, onUpdateDocList])
const { mutateAsync: doDeleteMetaData } = useDeleteMetaData(datasetId)
const handleDeleteMetaData = useCallback(async (metaDataId: string) => {
await doDeleteMetaData(metaDataId)
onUpdateDocList()
}, [doDeleteMetaData, onUpdateDocList])
const [builtInEnabled, setBuiltInEnabled] = useState(datasetMetaData?.built_in_field_enabled)
useEffect(() => { // wait for api response to set the right value
setBuiltInEnabled(datasetMetaData?.built_in_field_enabled)
}, [datasetMetaData])
const { mutateAsync: toggleBuiltInStatus } = useUpdateBuiltInStatus(datasetId)
const { data: builtInMetaData } = useBuiltInMetaDataFields()
return {
isShowEditModal,
showEditModal,
hideEditModal,
datasetMetaData: datasetMetaData?.doc_metadata,
handleAddMetaData,
handleRename,
handleDeleteMetaData,
builtInMetaData: builtInMetaData?.fields,
builtInEnabled,
setBuiltInEnabled: async (enable: boolean) => {
await toggleBuiltInStatus(enable)
setBuiltInEnabled(enable)
Toast.notify({
message: t('common.actionMsg.modifiedSuccessfully'),
type: 'success',
})
},
}
}
export default useEditDatasetMetadata

View File

@ -0,0 +1,159 @@
import { useBatchUpdateDocMetadata, useDatasetMetaData, useDocumentMetaData } from '@/service/knowledge/use-metadata'
import { useDatasetDetailContext } from '@/context/dataset-detail'
import type { BuiltInMetadataItem } from '../types'
import { DataType, type MetadataItemWithValue } from '../types'
import { useCallback, useState } from 'react'
import Toast from '@/app/components/base/toast'
import type { FullDocumentDetail } from '@/models/datasets'
import { useTranslation } from 'react-i18next'
import { useLanguages, useMetadataMap } from '@/hooks/use-metadata'
import { get } from 'lodash-es'
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
import useCheckMetadataName from './use-check-metadata-name'
type Props = {
datasetId: string
documentId: string
docDetail: FullDocumentDetail
}
const useMetadataDocument = ({
datasetId,
documentId,
docDetail,
}: Props) => {
const { t } = useTranslation()
const { dataset } = useDatasetDetailContext()
const embeddingAvailable = !!dataset?.embedding_available
const { mutateAsync } = useBatchUpdateDocMetadata()
const { checkName } = useCheckMetadataName()
const [isEdit, setIsEdit] = useState(false)
const { data: documentDetail } = useDocumentMetaData({
datasetId,
documentId,
})
const allList = documentDetail?.doc_metadata || []
const list = allList.filter(item => item.id !== 'built-in')
const builtList = allList.filter(item => item.id === 'built-in')
const [tempList, setTempList] = useState<MetadataItemWithValue[]>(list)
const { mutateAsync: doAddMetaData } = useCreateMetaData(datasetId)
const handleSelectMetaData = useCallback((metaData: MetadataItemWithValue) => {
setTempList((prev) => {
const index = prev.findIndex(item => item.id === metaData.id)
if (index === -1)
return [...prev, metaData]
return prev
})
}, [])
const handleAddMetaData = useCallback(async (payload: BuiltInMetadataItem) => {
const errorMsg = checkName(payload.name).errorMsg
if (errorMsg) {
Toast.notify({
message: errorMsg,
type: 'error',
})
return Promise.reject(new Error(errorMsg))
}
await doAddMetaData(payload)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}, [checkName, doAddMetaData, t])
const hasData = list.length > 0
const handleSave = async () => {
await mutateAsync({
dataset_id: datasetId,
metadata_list: [{
document_id: documentId,
metadata_list: tempList,
}],
})
setIsEdit(false)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
const handleCancel = () => {
setTempList(list)
setIsEdit(false)
}
const startToEdit = () => {
setTempList(list)
setIsEdit(true)
}
// built in enabled is set in dataset
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
const builtInEnabled = datasetMetaData?.built_in_field_enabled
// old metadata and technical params
const metadataMap = useMetadataMap()
const languageMap = useLanguages()
const getReadOnlyMetaData = (mainField: 'originInfo' | 'technicalParameters') => {
const fieldMap = metadataMap[mainField]?.subFieldsMap
const sourceData = docDetail
const getTargetMap = (field: string) => {
if (field === 'language')
return languageMap
return {} as any
}
const getTargetValue = (field: string) => {
const val = get(sourceData, field, '')
if (!val && val !== 0)
return '-'
if (fieldMap[field]?.inputType === 'select')
return getTargetMap(field)[val]
if (fieldMap[field]?.render)
return fieldMap[field]?.render?.(val, field === 'hit_count' ? get(sourceData, 'segment_count', 0) as number : undefined)
return val
}
const fieldList = Object.keys(fieldMap).map((key) => {
const field = fieldMap[key]
return {
id: field?.label,
type: DataType.string,
name: field?.label,
value: getTargetValue(key),
}
})
return fieldList
}
const originInfo = getReadOnlyMetaData('originInfo')
const technicalParameters = getReadOnlyMetaData('technicalParameters')
return {
embeddingAvailable,
isEdit,
setIsEdit,
list,
tempList,
setTempList,
handleSelectMetaData,
handleAddMetaData,
hasData,
builtList,
builtInEnabled,
startToEdit,
handleSave,
handleCancel,
originInfo,
technicalParameters,
}
}
export default useMetadataDocument

View File

@ -0,0 +1,89 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { DataType } from '../types'
import ModalLikeWrap from '../../../base/modal-like-wrap'
import Field from './field'
import OptionCard from '../../../workflow/nodes/_base/components/option-card'
import Input from '@/app/components/base/input'
import { RiArrowLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'dataset.metadata.createMetadata'
export type Props = {
onClose?: () => void
onSave: (data: any) => void
hasBack?: boolean
onBack?: () => void
}
const CreateContent: FC<Props> = ({
onClose = () => { },
hasBack,
onBack,
onSave,
}) => {
const { t } = useTranslation()
const [type, setType] = useState(DataType.string)
const handleTypeChange = useCallback((newType: DataType) => {
return () => setType(newType)
}, [setType])
const [name, setName] = useState('')
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}, [setName])
const handleSave = useCallback(() => {
onSave({
type,
name,
})
}, [onSave, type, name])
return (
<ModalLikeWrap
title={t(`${i18nPrefix}.title`)}
onClose={onClose}
onConfirm={handleSave}
hideCloseBtn={hasBack}
beforeHeader={hasBack && (
<div className='relative left-[-4px] mb-1 flex items-center py-1 space-x-1 text-text-accent cursor-pointer' onClick={onBack}>
<RiArrowLeftLine className='size-4' />
<div className='system-xs-semibold-uppercase'>{t(`${i18nPrefix}.back`)}</div>
</div>
)}
>
<div className='space-y-3'>
<Field label={t(`${i18nPrefix}.type`)}>
<div className='grid grid-cols-3 gap-2'>
<OptionCard
title='String'
selected={type === DataType.string}
onSelect={handleTypeChange(DataType.string)}
/>
<OptionCard
title='Number'
selected={type === DataType.number}
onSelect={handleTypeChange(DataType.number)}
/>
<OptionCard
title='Time'
selected={type === DataType.time}
onSelect={handleTypeChange(DataType.time)}
/>
</div>
</Field>
<Field label={t(`${i18nPrefix}.name`)}>
<Input
value={name}
onChange={handleNameChange}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
</div>
</ModalLikeWrap>
)
}
export default React.memo(CreateContent)

View File

@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../../base/portal-to-follow-elem'
type Props = {
open: boolean
setOpen: (open: boolean) => void
onSave: (data: any) => void
trigger: React.ReactNode
popupLeft?: number
} & CreateContentProps
const CreateMetadataModal: FC<Props> = ({
open,
setOpen,
trigger,
popupLeft = 20,
...createContentProps
}) => {
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='left-start'
offset={{
mainAxis: popupLeft,
crossAxis: -38,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<CreateContent {...createContentProps} onClose={() => setOpen(false)} />
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(CreateMetadataModal)

View File

@ -0,0 +1,248 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types'
import Drawer from '@/app/components/base/drawer'
import Button from '@/app/components/base/button'
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { getIcon } from '../utils/get-icon'
import cn from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Field from './field'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
import { useBoolean, useHover } from 'ahooks'
import Confirm from '@/app/components/base/confirm'
import Toast from '@/app/components/base/toast'
const i18nPrefix = 'dataset.metadata.datasetMetadata'
type Props = {
userMetadata: MetadataItemWithValueLength[]
builtInMetadata: BuiltInMetadataItem[]
isBuiltInEnabled: boolean
onIsBuiltInEnabledChange: (value: boolean) => void
onClose: () => void
onAdd: (payload: BuiltInMetadataItem) => void
onRename: (payload: MetadataItemWithValueLength) => void
onRemove: (metaDataId: string) => void
}
type ItemProps = {
readonly?: boolean
disabled?: boolean
payload: MetadataItemWithValueLength
onRename?: () => void
onDelete?: () => void
}
const Item: FC<ItemProps> = ({
readonly,
disabled,
payload,
onRename,
onDelete,
}) => {
const { t } = useTranslation()
const Icon = getIcon(payload.type)
const handleRename = useCallback(() => {
onRename?.()
}, [onRename])
const deleteBtnRef = useRef<HTMLDivElement>(null)
const isDeleteHovering = useHover(deleteBtnRef)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = useCallback(() => {
hideDeleteConfirm()
onDelete?.()
}, [hideDeleteConfirm, onDelete])
return (
<div
key={payload.name}
className={cn(
!readonly && !disabled && 'group/item hover:shadow-xs cursor-pointer',
'border border-components-panel-border-subtle rounded-md bg-components-panel-on-panel-item-bg',
isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
)}
>
<div
className={cn(
'flex items-center h-8 px-2 justify-between',
disabled && 'opacity-30', // not include border and bg
)}
>
<div className='flex items-center h-full text-text-tertiary space-x-1'>
<Icon className='shrink-0 size-4' />
<div className='max-w-[250px] truncate system-sm-medium text-text-primary'>{payload.name}</div>
<div className='shrink-0 system-xs-regular'>{payload.type}</div>
</div>
{(!readonly || disabled) && (
<div className='group-hover/item:hidden ml-2 shrink-0 system-xs-regular text-text-tertiary'>
{disabled ? t(`${i18nPrefix}.disabled`) : t(`${i18nPrefix}.values`, { num: payload.count || 0 })}
</div>
)}
<div className='group-hover/item:flex hidden ml-2 items-center text-text-tertiary space-x-1'>
<RiEditLine className='size-4 cursor-pointer' onClick={handleRename} />
<div ref={deleteBtnRef} className='hover:text-text-destructive'>
<RiDeleteBinLine className='size-4 cursor-pointer' onClick={showDeleteConfirm} />
</div>
</div>
{isShowDeleteConfirm && (
<Confirm
isShow
type='warning'
title={t('dataset.metadata.datasetMetadata.deleteTitle')}
content={t('dataset.metadata.datasetMetadata.deleteContent', { name: payload.name })}
onConfirm={handleDelete}
onCancel={hideDeleteConfirm}
/>
)}
</div>
</div>
)
}
const DatasetMetadataDrawer: FC<Props> = ({
userMetadata,
builtInMetadata,
isBuiltInEnabled,
onIsBuiltInEnabledChange,
onClose,
onAdd,
onRename,
onRemove,
}) => {
const { t } = useTranslation()
const [isShowRenameModal, setIsShowRenameModal] = useState(false)
const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
const [templeName, setTempleName] = useState('')
const handleRename = useCallback((payload: MetadataItemWithValueLength) => {
return () => {
setCurrPayload(payload)
setTempleName(payload.name)
setIsShowRenameModal(true)
}
}, [setCurrPayload, setIsShowRenameModal])
const [open, setOpen] = useState(false)
const handleAdd = useCallback(async (data: MetadataItemWithValueLength) => {
await onAdd(data)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setOpen(false)
}, [onAdd, t])
const handleRenamed = useCallback(async () => {
const item = userMetadata.find(p => p.id === currPayload?.id)
if (item) {
await onRename({
...item,
name: templeName,
})
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
setIsShowRenameModal(false)
}, [userMetadata, currPayload?.id, onRename, templeName, t])
const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
return async () => {
await onRemove(payload.id)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
}, [onRemove, t])
return (
<Drawer
isOpen={true}
onClose={onClose}
showClose
title={t('dataset.metadata.metadata')}
footer={null}
panelClassname='px-4 block !max-w-[420px] my-2 rounded-l-2xl'
>
<div className='h-full overflow-y-auto'>
<div className='system-sm-regular text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
<CreateModal
open={open}
setOpen={setOpen}
trigger={<Button variant='primary' className='mt-3'>
<RiAddLine className='mr-1' />
{t(`${i18nPrefix}.addMetaData`)}
</Button>} hasBack onSave={handleAdd}
/>
<div className='mt-3 space-y-1'>
{userMetadata.map(payload => (
<Item
key={payload.id}
payload={payload}
onRename={handleRename(payload)}
onDelete={handleDelete(payload)}
/>
))}
</div>
<div className='mt-3 flex h-6 items-center'>
<Switch
defaultValue={isBuiltInEnabled}
onChange={onIsBuiltInEnabledChange}
/>
<div className='ml-2 mr-0.5 system-sm-semibold text-text-secondary'>{t(`${i18nPrefix}.builtIn`)}</div>
<Tooltip popupContent={<div className='max-w-[100px]'>{t(`${i18nPrefix}.builtInDescription`)}</div>} />
</div>
<div className='mt-1 space-y-1'>
{builtInMetadata.map(payload => (
<Item
key={payload.name}
readonly
disabled={!isBuiltInEnabled}
payload={payload as MetadataItemWithValueLength}
/>
))}
</div>
{isShowRenameModal && (
<Modal isShow title={t(`${i18nPrefix}.rename`)} onClose={() => setIsShowRenameModal(false)}>
<Field label={t(`${i18nPrefix}.name`)} className='mt-4'>
<Input
value={templeName}
onChange={e => setTempleName(e.target.value)}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={() => {
setIsShowRenameModal(false)
setTempleName(currPayload!.name)
}}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleRenamed}
variant='primary'
disabled={!templeName}
>{t('common.operation.save')}</Button>
</div>
</Modal>
)}
</div>
</Drawer>
)
}
export default React.memo(DatasetMetadataDrawer)

View File

@ -0,0 +1,23 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
className?: string
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
className,
label,
children,
}) => {
return (
<div className={className}>
<div className='py-1 system-sm-semibold text-text-secondary'>{label}</div>
<div className='mt-1'>{children}</div>
</div>
)
}
export default React.memo(Field)

View File

@ -0,0 +1,81 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import SelectMetadata from './select-metadata'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../../base/portal-to-follow-elem'
import type { MetadataItem } from '../types'
import type { Placement } from '@floating-ui/react'
import { useDatasetMetaData } from '@/service/knowledge/use-metadata'
type Props = {
datasetId: string
popupPlacement?: Placement
popupOffset?: { mainAxis: number, crossAxis: number }
onSelect: (data: MetadataItem) => void
onSave: (data: MetadataItem) => void
trigger: React.ReactNode
onManage: () => void
} & CreateContentProps
enum Step {
select = 'select',
create = 'create',
}
const SelectMetadataModal: FC<Props> = ({
datasetId,
popupPlacement = 'left-start',
popupOffset = { mainAxis: -38, crossAxis: 4 },
trigger,
onSelect,
onSave,
onManage,
}) => {
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
const [open, setOpen] = useState(false)
const [step, setStep] = useState(Step.select)
const handleSave = useCallback(async (data: MetadataItem) => {
await onSave(data)
setStep(Step.select)
}, [onSave])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={popupPlacement}
offset={popupOffset}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
className='block'
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
{step === Step.select ? (
<SelectMetadata
onSelect={(data) => {
onSelect(data)
setOpen(false)
}}
list={datasetMetaData?.doc_metadata || []}
onNew={() => setStep(Step.create)}
onManage={onManage}
/>
) : (
<CreateContent
onSave={handleSave}
hasBack
onBack={() => setStep(Step.select)}
/>
)}
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(SelectMetadataModal)

View File

@ -0,0 +1,82 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import type { MetadataItem } from '../types'
import SearchInput from '@/app/components/base/search-input'
import { RiAddLine, RiArrowRightUpLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { getIcon } from '../utils/get-icon'
const i18nPrefix = 'dataset.metadata.selectMetadata'
type Props = {
list: MetadataItem[]
onSelect: (data: MetadataItem) => void
onNew: () => void
onManage: () => void
}
const SelectMetadata: FC<Props> = ({
list: notFilteredList,
onSelect,
onNew,
onManage,
}) => {
const { t } = useTranslation()
const [query, setQuery] = useState('')
const list = useMemo(() => {
if (!query) return notFilteredList
return notFilteredList.filter((item) => {
return item.name.toLowerCase().includes(query.toLowerCase())
})
}, [query, notFilteredList])
return (
<div className='w-[320px] pt-2 pb-0 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[5px]'>
<SearchInput
className='mx-2'
value={query}
onChange={setQuery}
placeholder={t(`${i18nPrefix}.search`)}
/>
<div className='mt-2'>
{list.map((item) => {
const Icon = getIcon(item.type)
return (
<div
key={item.id}
className='mx-1 flex items-center h-6 px-3 justify-between rounded-md hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect({
id: item.id,
name: item.name,
type: item.type,
})}
>
<div className='w-0 grow flex items-center h-full text-text-secondary'>
<Icon className='shrink-0 mr-[5px] size-3.5' />
<div className='w-0 grow truncate system-sm-medium'>{item.name}</div>
</div>
<div className='ml-1 shrink-0 system-xs-regular text-text-tertiary'>
{item.type}
</div>
</div>
)
})}
</div>
<div className='mt-1 flex justify-between p-1 border-t border-divider-subtle'>
<div className='flex items-center h-6 px-3 text-text-secondary rounded-md hover:bg-state-base-hover cursor-pointer space-x-1' onClick={onNew}>
<RiAddLine className='size-3.5' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.newAction`)}</div>
</div>
<div className='flex items-center h-6 text-text-secondary '>
<div className='mr-[3px] w-px h-3 bg-divider-regular'></div>
<div className='flex h-full items-center px-1.5 hover:bg-state-base-hover rounded-md cursor-pointer' onClick={onManage}>
<div className='mr-1 system-sm-medium'>{t(`${i18nPrefix}.manageAction`)}</div>
<RiArrowRightUpLine className='size-3.5' />
</div>
</div>
</div>
</div>
)
}
export default React.memo(SelectMetadata)

View File

@ -0,0 +1,26 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
label,
children,
}) => {
return (
<div className='flex items-start space-x-2'>
<div className='shrink-0 w-[128px] truncate py-1 items-center text-text-tertiary system-xs-medium'>
{label}
</div>
<div className='shrink-0 w-[244px]'>
{children}
</div>
</div>
)
}
export default React.memo(Field)

View File

@ -0,0 +1,120 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import InfoGroup from './info-group'
import NoData from './no-data'
import Button from '@/app/components/base/button'
import { RiEditLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import useMetadataDocument from '../hooks/use-metadata-document'
import type { FullDocumentDetail } from '@/models/datasets'
import cn from '@/utils/classnames'
const i18nPrefix = 'dataset.metadata.documentMetadata'
type Props = {
datasetId: string
documentId: string
className?: string
docDetail: FullDocumentDetail
}
const MetadataDocument: FC<Props> = ({
datasetId,
documentId,
className,
docDetail,
}) => {
const { t } = useTranslation()
const {
embeddingAvailable,
isEdit,
setIsEdit,
list,
tempList,
setTempList,
handleSelectMetaData,
handleAddMetaData,
hasData,
builtList,
builtInEnabled,
startToEdit,
handleSave,
handleCancel,
originInfo,
technicalParameters,
} = useMetadataDocument({ datasetId, documentId, docDetail })
return (
<div className={cn('w-[388px] space-y-4', className)}>
{(hasData || isEdit) ? (
<div className='pl-2'>
<InfoGroup
title={t('dataset.metadata.metadata')}
uppercaseTitle={false}
titleTooltip={t(`${i18nPrefix}.metadataToolTip`)}
list={isEdit ? tempList : list}
dataSetId={datasetId}
headerRight={embeddingAvailable && (isEdit ? (
<div className='flex space-x-1'>
<Button variant='ghost' size='small' onClick={handleCancel}>
<div>{t('common.operation.cancel')}</div>
</Button>
<Button variant='primary' size='small' onClick={handleSave}>
<div>{t('common.operation.save')}</div>
</Button>
</div>
) : (
<Button variant='ghost' size='small' onClick={startToEdit}>
<RiEditLine className='mr-1 size-3.5 text-text-tertiary cursor-pointer' />
<div>{t('common.operation.edit')}</div>
</Button>
))}
isEdit={isEdit}
contentClassName='mt-5'
onChange={(item) => {
const newList = tempList.map(i => (i.name === item.name ? item : i))
setTempList(newList)
}}
onDelete={(item) => {
const newList = tempList.filter(i => i.name !== item.name)
setTempList(newList)
}}
onAdd={handleAddMetaData}
onSelect={handleSelectMetaData}
/>
</div>
) : (
embeddingAvailable && <NoData onStart={() => setIsEdit(true)} />
)}
{builtInEnabled && (
<div className='pl-2'>
<Divider className='my-3' bgStyle='gradient' />
<InfoGroup
noHeader
titleTooltip='Built-in metadata is system-generated metadata that is automatically added to the document. You can enable or disable built-in metadata here.'
list={builtList}
dataSetId={datasetId}
/>
</div>
)}
{/* Old Metadata */}
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.documentInformation`)}
list={originInfo}
dataSetId={datasetId}
/>
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.technicalParameters`)}
list={technicalParameters}
dataSetId={datasetId}
/>
</div>
)
}
export default React.memo(MetadataDocument)

View File

@ -0,0 +1,111 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useRouter } from 'next/navigation'
import { DataType, type MetadataItemWithValue, isShowManageMetadataLocalStorageKey } from '../types'
import Field from './field'
import InputCombined from '../edit-metadata-batch/input-combined'
import { RiDeleteBinLine, RiQuestionLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
import AddMetadataButton from '../add-metadata-button'
import useTimestamp from '@/hooks/use-timestamp'
import { useTranslation } from 'react-i18next'
type Props = {
dataSetId: string
className?: string
noHeader?: boolean
title?: string
uppercaseTitle?: boolean
titleTooltip?: string
headerRight?: React.ReactNode
contentClassName?: string
list: MetadataItemWithValue[]
isEdit?: boolean
onChange?: (item: MetadataItemWithValue) => void
onDelete?: (item: MetadataItemWithValue) => void
onSelect?: (item: MetadataItemWithValue) => void
onAdd?: (item: MetadataItemWithValue) => void
}
const InfoGroup: FC<Props> = ({
dataSetId,
className,
noHeader,
title,
uppercaseTitle = true,
titleTooltip,
headerRight,
contentClassName,
list,
isEdit,
onChange,
onDelete,
onSelect,
onAdd,
}) => {
const router = useRouter()
const { t } = useTranslation()
const { formatTime: formatTimestamp } = useTimestamp()
const handleMangeMetadata = () => {
localStorage.setItem(isShowManageMetadataLocalStorageKey, 'true')
router.push(`/datasets/${dataSetId}/documents`)
}
return (
<div className={cn('bg-white', className)}>
{!noHeader && (
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-1'>
<div className={cn('text-text-secondary', uppercaseTitle ? 'system-xs-semibold-uppercase' : 'system-md-semibold')}>{title}</div>
{titleTooltip && (
<Tooltip popupContent={<div className='max-w-[240px]'>{titleTooltip}</div>}>
<div><RiQuestionLine className='size-3.5 text-text-tertiary' /></div>
</Tooltip>
)}
</div>
{headerRight}
</div>
)}
<div className={cn('mt-3 space-y-1', contentClassName)}>
{isEdit && (
<div>
<SelectMetadataModal
datasetId={dataSetId}
trigger={
<AddMetadataButton />
}
onSelect={data => onSelect?.(data as MetadataItemWithValue)}
onSave={data => onAdd?.(data)}
onManage={handleMangeMetadata}
/>
{list.length > 0 && <Divider className='my-3 ' bgStyle='gradient' />}
</div>
)}
{list.map((item, i) => (
<Field key={(item.id && item.id !== 'built-in') ? item.id : `${i}`} label={item.name}>
{isEdit ? (
<div className='flex items-center space-x-0.5'>
<InputCombined
className='h-6'
type={item.type}
value={item.value}
onChange={value => onChange?.({ ...item, value })}
/>
<div className='shrink-0 p-1 rounded-md text-text-tertiary hover:text-text-destructive hover:bg-state-destructive-hover cursor-pointer'>
<RiDeleteBinLine className='size-4' onClick={() => onDelete?.(item)} />
</div>
</div>
) : (<div className='py-1 system-xs-regular text-text-secondary'>{(item.value && item.type === DataType.time) ? formatTimestamp((item.value as number), t('datasetDocuments.metadata.dateTimeFormat')) : item.value}</div>)}
</Field>
))}
</div>
</div>
)
}
export default React.memo(InfoGroup)

View File

@ -0,0 +1,27 @@
'use client'
import Button from '@/app/components/base/button'
import { RiArrowRightLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
onStart: () => void
}
const NoData: FC<Props> = ({
onStart,
}) => {
const { t } = useTranslation()
return (
<div className='p-4 pt-3 rounded-xl bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2'>
<div className='text-text-secondary text-xs font-semibold leading-5'>{t('dataset.metadata.metadata')}</div>
<div className='mt-1 system-xs-regular text-text-tertiary'>{t('dataset.metadata.documentMetadata.metadataToolTip')}</div>
<Button variant='primary' className='mt-2' onClick={onStart}>
<div>{t('dataset.metadata.documentMetadata.startLabeling')}</div>
<RiArrowRightLine className='ml-1 size-4' />
</Button>
</div>
)
}
export default React.memo(NoData)

View File

@ -0,0 +1,41 @@
export enum DataType {
string = 'string',
number = 'number',
time = 'time',
}
export type BuiltInMetadataItem = {
type: DataType
name: string
}
export type MetadataItem = BuiltInMetadataItem & {
id: string
}
export type MetadataItemWithValue = MetadataItem & {
value: string | number | null
}
export type MetadataItemWithValueLength = MetadataItem & {
count: number
}
export type MetadataItemInBatchEdit = MetadataItemWithValue & {
isMultipleValue?: boolean
}
export type MetadataBatchEditToServer = { document_id: string, metadata_list: MetadataItemWithValue[] }[]
export enum UpdateType {
changeValue = 'changeValue',
delete = 'delete',
}
export type MetadataItemWithEdit = MetadataItemWithValue & {
isMultipleValue?: boolean
isUpdated?: boolean
updateType?: UpdateType
}
export const isShowManageMetadataLocalStorageKey = 'dify-isShowManageMetadata'

View File

@ -0,0 +1,10 @@
import { DataType } from '../types'
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
export const getIcon = (type: DataType) => {
return ({
[DataType.string]: RiTextSnippet,
[DataType.number]: RiHashtag,
[DataType.time]: RiTimeLine,
}[type] || RiTextSnippet)
}

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

View File

@ -2,13 +2,16 @@ import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { intersectionBy } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import useConfig from './use-config'
import RetrievalConfig from './components/retrieval-config'
import AddKnowledge from './components/add-dataset'
import DatasetList from './components/dataset-list'
import MetadataFilter from './components/metadata/metadata-filter'
import type { KnowledgeRetrievalNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
@ -35,6 +38,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
handleRetrievalModeChange,
handleMultipleRetrievalConfigChange,
selectedDatasets,
selectedDatasetsLoaded,
handleOnDatasetsChange,
isShowSingleRun,
hideSingleRun,
@ -46,15 +50,34 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
runResult,
rerankModelOpen,
setRerankModelOpen,
handleAddCondition,
handleMetadataFilterModeChange,
handleRemoveCondition,
handleToggleConditionLogicalOperator,
handleUpdateCondition,
handleMetadataModelChange,
handleMetadataCompletionParamsChange,
availableStringVars,
availableStringNodesWithParent,
availableNumberVars,
availableNumberNodesWithParent,
} = useConfig(id, data)
const handleOpenFromPropsChange = useCallback((openFromProps: boolean) => {
setRerankModelOpen(openFromProps)
}, [setRerankModelOpen])
const metadataList = useMemo(() => {
return intersectionBy(...selectedDatasets.filter((dataset) => {
return !!dataset.doc_metadata
}).map((dataset) => {
return dataset.doc_metadata!
}), 'name')
}, [selectedDatasets])
return (
<div className='pt-2'>
<div className='px-4 pb-4 space-y-4'>
<div className='px-4 pb-2 space-y-4'>
{/* {JSON.stringify(inputs, null, 2)} */}
<Field
title={t(`${i18nPrefix}.queryVariable`)}
@ -106,7 +129,26 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
/>
</Field>
</div>
<div className='mb-2 py-2'>
<MetadataFilter
metadataList={metadataList}
selectedDatasetsLoaded={selectedDatasetsLoaded}
metadataFilterMode={inputs.metadata_filtering_mode}
metadataFilteringConditions={inputs.metadata_filtering_conditions}
handleAddCondition={handleAddCondition}
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
handleRemoveCondition={handleRemoveCondition}
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
handleUpdateCondition={handleUpdateCondition}
metadataModelConfig={inputs.metadata_model_config}
handleMetadataModelChange={handleMetadataModelChange}
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
availableStringVars={availableStringVars}
availableStringNodesWithParent={availableStringNodesWithParent}
availableNumberVars={availableNumberVars}
availableNumberNodesWithParent={availableNumberNodesWithParent}
/>
</div>
<Split />
<div>
<OutputVars>

View File

@ -1,7 +1,14 @@
import type { CommonNodeType, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
import type {
CommonNodeType,
ModelConfig,
Node,
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import type { RETRIEVE_TYPE } from '@/types/app'
import type {
DataSet,
MetadataInDoc,
RerankingModeEnum,
} from '@/models/datasets'
@ -30,6 +37,61 @@ 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',
before = 'before',
after = 'after',
}
export enum MetadataFilteringModeEnum {
disabled = 'disabled',
automatic = 'automatic',
manual = 'manual',
}
export enum MetadataFilteringVariableType {
string = 'string',
number = 'number',
time = 'time',
}
export type MetadataFilteringCondition = {
id: string
name: string
comparison_operator: ComparisonOperator
value?: string | number
}
export type MetadataFilteringConditions = {
logical_operator: LogicalOperator
conditions: MetadataFilteringCondition[]
}
export type KnowledgeRetrievalNodeType = CommonNodeType & {
query_variable_selector: ValueSelector
dataset_ids: string[]
@ -37,4 +99,32 @@ export type KnowledgeRetrievalNodeType = CommonNodeType & {
multiple_retrieval_config?: MultipleRetrievalConfig
single_retrieval_config?: SingleRetrievalConfig
_datasets?: DataSet[]
metadata_filtering_mode?: MetadataFilteringModeEnum
metadata_filtering_conditions?: MetadataFilteringConditions
metadata_model_config?: ModelConfig
}
export type HandleAddCondition = (metadataItem: MetadataInDoc) => void
export type HandleRemoveCondition = (id: string) => void
export type HandleUpdateCondition = (id: string, newCondition: MetadataFilteringCondition) => void
export type HandleToggleConditionLogicalOperator = () => void
export type MetadataShape = {
metadataList?: MetadataInDoc[]
selectedDatasetsLoaded?: boolean
metadataFilteringConditions?: MetadataFilteringConditions
handleAddCondition: HandleAddCondition
handleRemoveCondition: HandleRemoveCondition
handleToggleConditionLogicalOperator: HandleToggleConditionLogicalOperator
handleUpdateCondition: HandleUpdateCondition
metadataModelConfig?: ModelConfig
handleMetadataModelChange?: (model: { modelId: string; provider: string; mode?: string; features?: string[] }) => void
handleMetadataCompletionParamsChange?: (params: Record<string, any>) => void
availableStringVars?: NodeOutPutVar[]
availableStringNodesWithParent?: Node[]
availableNumberVars?: NodeOutPutVar[]
availableNumberNodesWithParent?: Node[]
isCommonVariable?: boolean
availableCommonStringVars?: { name: string; type: string; }[]
availableCommonNumberVars?: { name: string; type: string; }[]
}

View File

@ -6,13 +6,28 @@ import {
} from 'react'
import produce from 'immer'
import { isEqual } from 'lodash-es'
import { v4 as uuid4 } from 'uuid'
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,
MetadataFilteringVariableType,
} from './types'
import {
getMultipleRetrievalConfig,
getSelectedDatasetsMode,
@ -25,6 +40,7 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@ -196,13 +212,14 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
setInputs(newInputs)
}, [inputs, setInputs, selectedDatasets, currentRerankModel, currentRerankProvider])
const [selectedDatasetsLoaded, setSelectedDatasetsLoaded] = useState(false)
// datasets
useEffect(() => {
(async () => {
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) => {
@ -210,6 +227,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
draft._datasets = selectedDatasets
})
setInputs(newInputs)
setSelectedDatasetsLoaded(true)
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -287,6 +305,113 @@ 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, type }) => {
let operator: ComparisonOperator = ComparisonOperator.is
if (type === MetadataFilteringVariableType.number)
operator = ComparisonOperator.equal
const newCondition = {
id: uuid4(),
name,
comparison_operator: operator,
}
const newInputs = produce(inputRef.current, (draft) => {
if (draft.metadata_filtering_conditions) {
draft.metadata_filtering_conditions.conditions.push(newCondition)
}
else {
draft.metadata_filtering_conditions = {
logical_operator: LogicalOperator.and,
conditions: [newCondition],
}
}
})
setInputs(newInputs)
}, [setInputs])
const handleRemoveCondition = useCallback<HandleRemoveCondition>((id) => {
const conditions = inputRef.current.metadata_filtering_conditions?.conditions || []
const index = conditions.findIndex(c => c.id === id)
const newInputs = produce(inputRef.current, (draft) => {
if (index > -1)
draft.metadata_filtering_conditions?.conditions.splice(index, 1)
})
setInputs(newInputs)
}, [setInputs])
const handleUpdateCondition = useCallback<HandleUpdateCondition>((id, newCondition) => {
const conditions = inputRef.current.metadata_filtering_conditions?.conditions || []
const index = conditions.findIndex(c => c.id === id)
const newInputs = produce(inputRef.current, (draft) => {
if (index > -1)
draft.metadata_filtering_conditions!.conditions[index] = newCondition
})
setInputs(newInputs)
}, [setInputs])
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>(() => {
const oldLogicalOperator = inputRef.current.metadata_filtering_conditions?.logical_operator
const newLogicalOperator = oldLogicalOperator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
const newInputs = produce(inputRef.current, (draft) => {
draft.metadata_filtering_conditions!.logical_operator = newLogicalOperator
})
setInputs(newInputs)
}, [setInputs])
const handleMetadataModelChange = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.metadata_model_config = {
provider: model.provider,
name: model.modelId,
mode: model.mode || 'chat',
completion_params: draft.metadata_model_config?.completion_params || { temperature: 0.7 },
}
})
setInputs(newInputs)
}, [setInputs])
const handleMetadataCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.metadata_model_config = {
...draft.metadata_model_config!,
completion_params: newParams,
}
})
setInputs(newInputs)
}, [setInputs])
const filterStringVar = useCallback((varPayload: Var) => {
return [VarType.string].includes(varPayload.type)
}, [])
const {
availableVars: availableStringVars,
availableNodesWithParent: availableStringNodesWithParent,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterStringVar,
})
const filterNumberVar = useCallback((varPayload: Var) => {
return [VarType.number].includes(varPayload.type)
}, [])
const {
availableVars: availableNumberVars,
availableNodesWithParent: availableNumberNodesWithParent,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterNumberVar,
})
return {
readOnly,
inputs,
@ -297,6 +422,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
handleModelChanged,
handleCompletionParamsChange,
selectedDatasets: selectedDatasets.filter(d => d.name),
selectedDatasetsLoaded,
handleOnDatasetsChange,
isShowSingleRun,
hideSingleRun,
@ -308,6 +434,17 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
runResult,
rerankModelOpen,
setRerankModelOpen,
handleMetadataFilterModeChange,
handleUpdateCondition,
handleAddCondition,
handleRemoveCondition,
handleToggleConditionLogicalOperator,
handleMetadataModelChange,
handleMetadataCompletionParamsChange,
availableStringVars,
availableStringNodesWithParent,
availableNumberVars,
availableNumberNodesWithParent,
}
}

View File

@ -1,3 +1,4 @@
import type { RefObject } from 'react'
import { createContext, useContext } from 'use-context-selector'
import { PromptMode } from '@/models/debug'
import type {
@ -92,6 +93,7 @@ type IDebugConfiguration = {
showSelectDataSet: () => void
// dataset config
datasetConfigs: DatasetConfigs
datasetConfigsRef: RefObject<DatasetConfigs>
setDatasetConfigs: (config: DatasetConfigs) => void
hasSetContextVar: boolean
isShowVisionConfig: boolean
@ -236,6 +238,9 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
datasets: [],
},
},
datasetConfigsRef: {
current: null,
},
setDatasetConfigs: () => { },
hasSetContextVar: false,
isShowVisionConfig: false,

View File

@ -240,7 +240,7 @@ export const useMetadataMap = (): MetadataMap => {
},
'data_source_type': {
label: t(`${fieldPrefix}.originInfo.source`),
render: value => t(`datasetDocuments.metadata.source.${value}`),
render: value => t(`datasetDocuments.metadata.source.${value === 'notion_import' ? 'notion' : value}`),
},
},
},

View File

@ -56,6 +56,7 @@ const translation = {
documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit',
documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ',
documentProcessingPriority: ' Document Processing',
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
priority: {
'standard': 'Standard',
'priority': 'Priority',

View File

@ -168,6 +168,54 @@ const translation = {
preprocessDocument: '{{num}} Preprocess Documents',
allKnowledge: 'All Knowledge',
allKnowledgeDescription: 'Select to display all knowledge in this workspace. Only the Workspace Owner can manage all knowledge.',
embeddingModelNotAvailable: 'Embedding model is unavailable.',
metadata: {
metadata: 'Metadata',
addMetadata: 'Add Metadata',
chooseTime: 'Choose a time...',
createMetadata: {
title: 'New Metadata',
back: 'Back',
type: 'Type',
name: 'Name',
namePlaceholder: 'Add metadata name',
},
checkName: {
empty: 'Metadata name cannot be empty',
invalid: 'Metadata name can only contain lowercase letters, numbers, and underscores and must start with a lowercase letter',
},
batchEditMetadata: {
editMetadata: 'Edit Metadata',
editDocumentsNum: 'Editing {{num}} documents',
applyToAllSelectDocument: 'Apply to all selected documents',
applyToAllSelectDocumentTip: 'Automatically create all the above edited and new metadata for all selected documents, otherwise editing metadata will only apply to documents with it.',
multipleValue: 'Multiple Value',
},
selectMetadata: {
search: 'Search metadata',
newAction: 'New Metadata',
manageAction: 'Manage',
},
datasetMetadata: {
description: 'You can manage all metadata in this knowledge here. Modifications will be synchronized to every document.',
addMetaData: 'Add Metadata',
values: '{{num}} Values',
disabled: 'Disabled',
rename: 'Rename',
name: 'Name',
namePlaceholder: 'Metadata name',
builtIn: 'Built-in',
builtInDescription: 'Built-in metadata is automatically extracted and generated. It must be enabled before use and cannot be edited.',
deleteTitle: 'Confirm to delete',
deleteContent: 'Are you sure you want to delete the metadata "{{name}}"',
},
documentMetadata: {
metadataToolTip: 'Metadata serves as a critical filter that enhances the accuracy and relevance of information retrieval. You can modify and add metadata for this document here.',
startLabeling: 'Start Labeling',
documentInformation: 'Document Information',
technicalParameters: 'Technical Parameters',
},
},
}
export default translation

View File

@ -429,6 +429,34 @@ const translation = {
url: 'Segmented URL',
metadata: 'Other metadata',
},
metadata: {
title: 'Metadata Filtering',
tip: '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.',
options: {
disabled: {
title: 'Disabled',
subTitle: 'Not enabling metadata filtering',
},
automatic: {
title: 'Automatic',
subTitle: 'Automatically generate metadata filtering conditions based on user query',
desc: 'Automatically generate metadata filtering conditions based on Query Variable',
},
manual: {
title: 'Manual',
subTitle: 'Manually add metadata filtering conditions',
},
},
panel: {
title: 'Metadata Filter Conditions',
conditions: 'Conditions',
add: 'Add Condition',
search: 'Search metadata',
placeholder: 'Enter value',
datePlaceholder: 'Choose a time...',
select: 'Select variable...',
},
},
},
http: {
inputVars: 'Input Variables',
@ -517,6 +545,8 @@ const translation = {
'all of': 'all of',
'exists': 'exists',
'not exists': 'not exists',
'before': 'before',
'after': 'after',
},
optionName: {
image: 'Image',

View File

@ -55,6 +55,7 @@ const translation = {
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが1分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが1分間に10回連続でヒットテストを実行した場合、そのワークスペースは次の1分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
documentProcessingPriority: '文書処理',
documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。',
priority: {
'standard': '標準',
'priority': '優先',

View File

@ -55,6 +55,7 @@ const translation = {
documentsRequestQuota: '{{count,number}}/分钟 知识库请求频率限制',
documentsRequestQuotaTooltip: '指每分钟内一个空间在知识库中可执行的操作总数包括数据集的创建、删除、更新文档的上传、修改、归档以及知识库查询等用于评估知识库请求的性能。例如Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。',
documentProcessingPriority: '文档处理',
documentProcessingPriorityUpgrade: '以更快的速度、更高的精度处理更多的数据。',
priority: {
'standard': '标准',
'priority': '优先',

View File

@ -168,6 +168,54 @@ const translation = {
preprocessDocument: '{{num}} 个预处理文档',
allKnowledge: '所有知识库',
allKnowledgeDescription: '选择以显示该工作区内所有知识库。只有工作区所有者才能管理所有知识库。',
embeddingModelNotAvailable: 'Embedding 模型不可用。',
metadata: {
metadata: '元数据',
addMetadata: '添加元数据',
chooseTime: '选择时间',
createMetadata: {
title: '新建元数据',
back: '返回',
type: '类型',
name: '名称',
namePlaceholder: '添加元数据名称',
},
checkName: {
empty: '元数据名称不能为空',
invalid: '元数据名称只能包含小写字母、数字和下划线,并且必须以小写字母开头',
},
batchEditMetadata: {
editMetadata: '编辑元数据',
editDocumentsNum: '编辑 {{num}} 个文档',
applyToAllSelectDocument: '应用于所有选定文档',
applyToAllSelectDocumentTip: '自动为所有选定文档创建上述编辑和新元数据,否则仅对具有元数据的文档应用编辑。',
multipleValue: '多个值',
},
selectMetadata: {
search: '搜索元数据',
newAction: '新建元数据',
manageAction: '管理',
},
datasetMetadata: {
description: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
addMetaData: '添加元数据',
values: '{{num}} 个值',
disabled: '已禁用',
rename: '重命名',
name: '名称',
namePlaceholder: '元数据名称',
builtIn: '内置',
builtInDescription: '内置元数据是系统预定义的元数据,您可以在此处查看和管理内置元数据。',
deleteTitle: '确定删除',
deleteContent: '你确定要删除元数据 "{{name}}" 吗?',
},
documentMetadata: {
metadataToolTip: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
startLabeling: '开始标注',
documentInformation: '文档信息',
technicalParameters: '技术参数',
},
},
}
export default translation

View File

@ -430,6 +430,34 @@ const translation = {
url: '分段链接',
metadata: '其他元数据',
},
metadata: {
title: '元数据过滤',
tip: '元数据过滤是使用元数据属性(例如标签、类别或访问权限)来细化和控制系统内相关信息的检索过程。',
options: {
disabled: {
title: '禁用',
subTitle: '禁用元数据过滤',
},
automatic: {
title: '自动',
subTitle: '根据用户查询自动生成元数据过滤条件',
desc: '根据 Query Variable 自动生成元数据过滤条件',
},
manual: {
title: '手动',
subTitle: '手动添加元数据过滤条件',
},
},
panel: {
title: '元数据过滤条件',
conditions: '条件',
add: '添加条件',
search: '搜索元数据',
placeholder: '输入值',
datePlaceholder: '选择日期...',
select: '选择变量...',
},
},
},
http: {
inputVars: '输入变量',
@ -518,6 +546,8 @@ const translation = {
'all of': '全部是',
'exists': '存在',
'not exists': '不存在',
'before': '早于',
'after': '晚于',
},
optionName: {
image: '图片',

View File

@ -2,6 +2,8 @@ import type { DataSourceNotionPage, DataSourceProvider } from './common'
import type { AppIconType, AppMode, RetrievalConfig } from '@/types/app'
import type { Tag } from '@/app/components/base/tag-management/constant'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types'
export enum DataSourceType {
FILE = 'upload_file',
@ -21,6 +23,13 @@ export enum ChunkingMode {
parentChild = 'hierarchical_model', // Parent-Child
}
export type MetadataInDoc = {
value: string
id: string
type: MetadataFilteringVariableType
name: string
}
export type DataSet = {
id: string
name: string
@ -56,6 +65,8 @@ export type DataSet = {
score_threshold: number
score_threshold_enabled: boolean
}
built_in_field_enabled: boolean
doc_metadata?: MetadataInDoc[]
}
export type ExternalAPIItem = {
@ -314,6 +325,7 @@ export type SimpleDocumentDetail = InitialDocumentDetail & {
extension: string
}
}
doc_metadata?: MetadataItemWithValue[]
}
export type DocumentListResponse = {

View File

@ -3,6 +3,11 @@ import type {
RerankingModeEnum,
} from '@/models/datasets'
import type { FileUpload } from '@/app/components/base/features/types'
import type {
MetadataFilteringConditions,
MetadataFilteringModeEnum,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { ModelConfig as NodeModelConfig } from '@/app/components/workflow/types'
export type Inputs = Record<string, string | number | object>
export enum PromptMode {
@ -10,25 +15,25 @@ export enum PromptMode {
advanced = 'advanced',
}
export interface PromptItem {
export type PromptItem = {
role?: PromptRole
text: string
}
export interface ChatPromptConfig {
export type ChatPromptConfig = {
prompt: PromptItem[]
}
export interface ConversationHistoriesRole {
export type ConversationHistoriesRole = {
user_prefix: string
assistant_prefix: string
}
export interface CompletionPromptConfig {
export type CompletionPromptConfig = {
prompt: PromptItem
conversation_histories_role: ConversationHistoriesRole
}
export interface BlockStatus {
export type BlockStatus = {
context: boolean
history: boolean
query: boolean
@ -40,7 +45,7 @@ export enum PromptRole {
assistant = 'assistant',
}
export interface PromptVariable {
export type PromptVariable = {
key: string
name: string
type: string // "string" | "number" | "select",
@ -55,7 +60,7 @@ export interface PromptVariable {
icon_background?: string
}
export interface CompletionParams {
export type CompletionParams = {
max_tokens: number
temperature: number
top_p: number
@ -66,12 +71,12 @@ export interface CompletionParams {
export type ModelId = 'gpt-3.5-turbo' | 'text-davinci-003'
export interface PromptConfig {
export type PromptConfig = {
prompt_template: string
prompt_variables: PromptVariable[]
}
export interface MoreLikeThisConfig {
export type MoreLikeThisConfig = {
enabled: boolean
}
@ -79,7 +84,7 @@ export type SuggestedQuestionsAfterAnswerConfig = MoreLikeThisConfig
export type SpeechToTextConfig = MoreLikeThisConfig
export interface TextToSpeechConfig {
export type TextToSpeechConfig = {
enabled: boolean
voice?: string
language?: string
@ -88,7 +93,7 @@ export interface TextToSpeechConfig {
export type CitationConfig = MoreLikeThisConfig
export interface AnnotationReplyConfig {
export type AnnotationReplyConfig = {
id: string
enabled: boolean
score_threshold: number
@ -98,7 +103,7 @@ export interface AnnotationReplyConfig {
}
}
export interface ModerationContentConfig {
export type ModerationContentConfig = {
enabled: boolean
preset_response?: string
}
@ -113,14 +118,14 @@ export type ModerationConfig = MoreLikeThisConfig & {
}
export type RetrieverResourceConfig = MoreLikeThisConfig
export interface AgentConfig {
export type AgentConfig = {
enabled: boolean
strategy: AgentStrategy
max_iteration: number
tools: ToolItem[]
}
// frontend use. Not the same as backend
export interface ModelConfig {
export type ModelConfig = {
provider: string // LLM Provider: for example "OPENAI"
model_id: string
mode: ModelModeType
@ -138,12 +143,12 @@ export interface ModelConfig {
dataSets: any[]
agentConfig: AgentConfig
}
export interface DatasetConfigItem {
export type DatasetConfigItem = {
enable: boolean
value: number
}
export interface DatasetConfigs {
export type DatasetConfigs = {
retrieval_model: RETRIEVE_TYPE
reranking_model: {
reranking_provider_name: string
@ -170,41 +175,44 @@ export interface DatasetConfigs {
}
}
reranking_enable?: boolean
metadata_filtering_mode?: MetadataFilteringModeEnum
metadata_filtering_conditions?: MetadataFilteringConditions
metadata_model_config?: NodeModelConfig
}
export interface DebugRequestBody {
export type DebugRequestBody = {
inputs: Inputs
query: string
completion_params: CompletionParams
model_config: ModelConfig
}
export interface DebugResponse {
export type DebugResponse = {
id: string
answer: string
created_at: string
}
export interface DebugResponseStream {
export type DebugResponseStream = {
id: string
data: string
created_at: string
}
export interface FeedBackRequestBody {
export type FeedBackRequestBody = {
message_id: string
rating: 'like' | 'dislike'
content?: string
from_source: 'api' | 'log'
}
export interface FeedBackResponse {
export type FeedBackResponse = {
message_id: string
rating: 'like' | 'dislike'
}
// Log session list
export interface LogSessionListQuery {
export type LogSessionListQuery = {
keyword?: string
start?: string // format datetime(YYYY-mm-dd HH:ii)
end?: string // format datetime(YYYY-mm-dd HH:ii)
@ -212,7 +220,7 @@ export interface LogSessionListQuery {
limit: number // default 20. 1-100
}
export interface LogSessionListResponse {
export type LogSessionListResponse = {
data: {
id: string
conversation_id: string
@ -226,7 +234,7 @@ export interface LogSessionListResponse {
}
// log session detail and debug
export interface LogSessionDetailResponse {
export type LogSessionDetailResponse = {
id: string
conversation_id: string
model_provider: string
@ -240,7 +248,7 @@ export interface LogSessionDetailResponse {
from_source: 'api' | 'log'
}
export interface SavedMessage {
export type SavedMessage = {
id: string
answer: string
}

View File

@ -11,7 +11,7 @@ import type { CommonResponse } from '@/models/common'
const NAME_SPACE = 'knowledge/document'
const useDocumentListKey = [NAME_SPACE, 'documentList']
export const useDocumentListKey = [NAME_SPACE, 'documentList']
export const useDocumentList = (payload: {
datasetId: string
query: {

View File

@ -0,0 +1,146 @@
import type { BuiltInMetadataItem, MetadataBatchEditToServer, MetadataItemWithValueLength } from '@/app/components/datasets/metadata/types'
import { del, get, patch, post } from '../base'
import { useDocumentListKey, useInvalidDocumentList } from './use-document'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useInvalid } from '../use-base'
import type { DocumentDetailResponse } from '@/models/datasets'
const NAME_SPACE = 'dataset-metadata'
export const useDatasetMetaData = (datasetId: string) => {
return useQuery<{ doc_metadata: MetadataItemWithValueLength[], built_in_field_enabled: boolean }>({
queryKey: [NAME_SPACE, 'dataset', datasetId],
queryFn: () => {
return get<{ doc_metadata: MetadataItemWithValueLength[], built_in_field_enabled: boolean }>(`/datasets/${datasetId}/metadata`)
},
})
}
export const useInvalidDatasetMetaData = (datasetId: string) => {
return useInvalid([NAME_SPACE, 'dataset', datasetId])
}
export const useCreateMetaData = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
return useMutation({
mutationFn: async (payload: BuiltInMetadataItem) => {
await post(`/datasets/${datasetId}/metadata`, {
body: payload,
})
await invalidDatasetMetaData()
return Promise.resolve(true)
},
})
}
export const useInvalidAllDocumentMetaData = (datasetId: string) => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'document', datasetId],
exact: false, // invalidate all document metadata: [NAME_SPACE, 'document', datasetId, documentId]
})
}
}
const useInvalidAllMetaData = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
const invalidDocumentList = useInvalidDocumentList(datasetId)
const invalidateAllDocumentMetaData = useInvalidAllDocumentMetaData(datasetId)
return async () => {
// meta data in dataset
await invalidDatasetMetaData()
// meta data in document list
invalidDocumentList()
// meta data in single document
await invalidateAllDocumentMetaData() // meta data in document
}
}
export const useRenameMeta = (datasetId: string) => {
const invalidateAllMetaData = useInvalidAllMetaData(datasetId)
return useMutation({
mutationFn: async (payload: MetadataItemWithValueLength) => {
await patch(`/datasets/${datasetId}/metadata/${payload.id}`, {
body: {
name: payload.name,
},
})
await invalidateAllMetaData()
},
})
}
export const useDeleteMetaData = (datasetId: string) => {
const invalidateAllMetaData = useInvalidAllMetaData(datasetId)
return useMutation({
mutationFn: async (metaDataId: string) => {
// datasetMetaData = datasetMetaData.filter(item => item.id !== metaDataId)
await del(`/datasets/${datasetId}/metadata/${metaDataId}`)
await invalidateAllMetaData()
},
})
}
export const useBuiltInMetaDataFields = () => {
return useQuery<{ fields: BuiltInMetadataItem[] }>({
queryKey: [NAME_SPACE, 'built-in'],
queryFn: () => {
return get('/datasets/metadata/built-in')
},
})
}
export const useDocumentMetaData = ({ datasetId, documentId }: { datasetId: string, documentId: string }) => {
return useQuery<DocumentDetailResponse>({
queryKey: [NAME_SPACE, 'document', datasetId, documentId],
queryFn: () => {
return get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params: { metadata: 'only' } })
},
})
}
export const useBatchUpdateDocMetadata = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (payload: {
dataset_id: string
metadata_list: MetadataBatchEditToServer
}) => {
const documentIds = payload.metadata_list.map(item => item.document_id)
await post(`/datasets/${payload.dataset_id}/documents/metadata`, {
body: {
operation_data: payload.metadata_list,
},
})
// meta data in dataset
await queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'dataset', payload.dataset_id],
})
// meta data in document list
await queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'dataset', payload.dataset_id],
})
await queryClient.invalidateQueries({
queryKey: [...useDocumentListKey, payload.dataset_id],
})
// meta data in single document
await Promise.all(documentIds.map(documentId => queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'document', payload.dataset_id, documentId],
},
)))
},
})
}
export const useUpdateBuiltInStatus = (datasetId: string) => {
const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId)
return useMutation({
mutationFn: async (enabled: boolean) => {
await post(`/datasets/${datasetId}/metadata/built-in/${enabled ? 'enable' : 'disable'}`)
invalidDatasetMetaData()
},
})
}