소스 검색

feat: upgrade knowledge metadata (#16063)

Support filter knowledge by metadata.

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: NFish <douxc512@gmail.com>
zxhlyh 3 달 전
부모
커밋
20376ca951
72개의 변경된 파일4775개의 추가작업 그리고 101개의 파일을 삭제
  1. 249 0
      web/app/(commonLayout)/datasets/template/template.en.mdx
  2. 248 0
      web/app/(commonLayout)/datasets/template/template.zh.mdx
  3. 129 1
      web/app/components/app/configuration/dataset-config/index.tsx
  4. 8 2
      web/app/components/app/configuration/index.tsx
  5. 7 3
      web/app/components/base/date-and-time-picker/date-picker/index.tsx
  6. 3 1
      web/app/components/base/date-and-time-picker/types.ts
  7. 11 9
      web/app/components/base/drawer/index.tsx
  8. 15 14
      web/app/components/base/input-number/index.tsx
  9. 58 0
      web/app/components/base/modal-like-wrap/index.tsx
  10. 11 6
      web/app/components/datasets/common/document-status-with-action/status-with-action.tsx
  11. 12 1
      web/app/components/datasets/documents/detail/completed/common/batch-action.tsx
  12. 4 3
      web/app/components/datasets/documents/detail/index.tsx
  13. 42 2
      web/app/components/datasets/documents/index.tsx
  14. 41 8
      web/app/components/datasets/documents/list.tsx
  15. 31 0
      web/app/components/datasets/metadata/add-metadata-button.tsx
  16. 76 0
      web/app/components/datasets/metadata/base/date-picker.tsx
  17. 45 0
      web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx
  18. 56 0
      web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx
  19. 36 0
      web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx
  20. 61 0
      web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx
  21. 34 0
      web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx
  22. 27 0
      web/app/components/datasets/metadata/edit-metadata-batch/label.tsx
  23. 189 0
      web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx
  24. 143 0
      web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts
  25. 28 0
      web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts
  26. 96 0
      web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts
  27. 159 0
      web/app/components/datasets/metadata/hooks/use-metadata-document.ts
  28. 89 0
      web/app/components/datasets/metadata/metadata-dataset/create-content.tsx
  29. 45 0
      web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx
  30. 248 0
      web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx
  31. 23 0
      web/app/components/datasets/metadata/metadata-dataset/field.tsx
  32. 81 0
      web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx
  33. 82 0
      web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx
  34. 26 0
      web/app/components/datasets/metadata/metadata-document/field.tsx
  35. 120 0
      web/app/components/datasets/metadata/metadata-document/index.tsx
  36. 111 0
      web/app/components/datasets/metadata/metadata-document/info-group.tsx
  37. 27 0
      web/app/components/datasets/metadata/metadata-document/no-data.tsx
  38. 41 0
      web/app/components/datasets/metadata/types.ts
  39. 10 0
      web/app/components/datasets/metadata/utils/get-icon.ts
  40. 95 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx
  41. 91 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx.tsx
  42. 86 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx
  43. 192 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx
  44. 88 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx
  45. 98 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx
  46. 84 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-string.tsx
  47. 71 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx
  48. 92 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx
  49. 75 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/index.tsx
  50. 65 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts
  51. 101 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx
  52. 106 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx
  53. 39 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-icon.tsx
  54. 51 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-panel.tsx
  55. 69 0
      web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-trigger.tsx
  56. 44 2
      web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
  57. 91 1
      web/app/components/workflow/nodes/knowledge-retrieval/types.ts
  58. 140 3
      web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
  59. 5 0
      web/context/debug-configuration.ts
  60. 19 19
      web/hooks/use-metadata.ts
  61. 1 0
      web/i18n/en-US/billing.ts
  62. 48 0
      web/i18n/en-US/dataset.ts
  63. 30 0
      web/i18n/en-US/workflow.ts
  64. 1 0
      web/i18n/ja-JP/billing.ts
  65. 1 0
      web/i18n/zh-Hans/billing.ts
  66. 48 0
      web/i18n/zh-Hans/dataset.ts
  67. 30 0
      web/i18n/zh-Hans/workflow.ts
  68. 12 0
      web/models/datasets.ts
  69. 33 25
      web/models/debug.ts
  70. 0 0
      web/service/knowledge/use-dateset.ts
  71. 1 1
      web/service/knowledge/use-document.ts
  72. 146 0
      web/service/knowledge/use-metadata.ts

+ 249 - 0
web/app/(commonLayout)/datasets/template/template.en.mdx

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

+ 248 - 0
web/app/(commonLayout)/datasets/template/template.zh.mdx

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

+ 129 - 1
web/app/components/app/configuration/dataset-config/index.tsx

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

+ 8 - 2
web/app/components/app/configuration/index.tsx

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

+ 7 - 3
web/app/components/base/date-and-time-picker/date-picker/index.tsx

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

+ 3 - 1
web/app/components/base/date-and-time-picker/types.ts

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

+ 11 - 9
web/app/components/base/drawer/index.tsx

@@ -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)}>
           <>
-            {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">
-              <XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
-            </Dialog.Title>}
+            <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 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}
           </>

+ 15 - 14
web/app/components/base/input-number/index.tsx

@@ -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(
-          size === 'sm' ? 'pt-1' : 'pt-1.5',
-          'px-1.5 hover:bg-components-input-bg-hover',
-          disabled && 'cursor-not-allowed hover:bg-transparent',
-        )}
-      >
+      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>

+ 58 - 0
web/app/components/base/modal-like-wrap/index.tsx

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

+ 11 - 6
web/app/components/datasets/common/document-status-with-action/status-with-action.tsx

@@ -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,17 +47,22 @@ 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%)]')
-      }`}
+        }`}
       />
       <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>
-        <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>
+        {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>
   )

+ 12 - 1
web/app/components/datasets/documents/detail/completed/common/batch-action.tsx

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

+ 4 - 3
web/app/components/datasets/documents/detail/index.tsx

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

+ 42 - 2
web/app/components/datasets/documents/index.tsx

@@ -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 ?? ''}>
@@ -116,7 +119,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
       if (totalPages < currPage + 1)
         setCurrPage(totalPages === 0 ? 0 : totalPages - 1)
     }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [documentsRes])
 
   const invalidDocumentDetail = useInvalidDocumentDetailKey()
@@ -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'} />
         }

+ 41 - 8
web/app/components/datasets/documents/list.tsx

@@ -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()}>
-                  <Checkbox
-                    className='shrink-0 mr-2'
-                    checked={isAllSelected}
-                    mixed={!isAllSelected && isSomeSelected}
-                    onCheck={onSelectedAll}
-                  />
+                  {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>
   )
 }

+ 31 - 0
web/app/components/datasets/metadata/add-metadata-button.tsx

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

+ 76 - 0
web/app/components/datasets/metadata/base/date-picker.tsx

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

+ 45 - 0
web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx

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

+ 56 - 0
web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx

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

+ 36 - 0
web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx

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

+ 61 - 0
web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx

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

+ 34 - 0
web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx

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

+ 27 - 0
web/app/components/datasets/metadata/edit-metadata-batch/label.tsx

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

+ 189 - 0
web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx

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

+ 143 - 0
web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts

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

+ 28 - 0
web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts

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

+ 96 - 0
web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts

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

+ 159 - 0
web/app/components/datasets/metadata/hooks/use-metadata-document.ts

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

+ 89 - 0
web/app/components/datasets/metadata/metadata-dataset/create-content.tsx

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

+ 45 - 0
web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx

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

+ 248 - 0
web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx

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

+ 23 - 0
web/app/components/datasets/metadata/metadata-dataset/field.tsx

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

+ 81 - 0
web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx

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

+ 82 - 0
web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx

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

+ 26 - 0
web/app/components/datasets/metadata/metadata-document/field.tsx

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

+ 120 - 0
web/app/components/datasets/metadata/metadata-document/index.tsx

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

+ 111 - 0
web/app/components/datasets/metadata/metadata-document/info-group.tsx

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

+ 27 - 0
web/app/components/datasets/metadata/metadata-document/no-data.tsx

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

+ 41 - 0
web/app/components/datasets/metadata/types.ts

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

+ 10 - 0
web/app/components/datasets/metadata/utils/get-icon.ts

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

+ 95 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx

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

+ 91 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx.tsx

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

+ 86 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-date.tsx

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

+ 192 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx

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

+ 88 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx

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

+ 98 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx

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

+ 84 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-string.tsx

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

+ 71 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx

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

+ 92 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx

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

+ 75 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/index.tsx

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

+ 65 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils.ts

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

+ 101 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx

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

+ 106 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx

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

+ 39 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-icon.tsx

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

+ 51 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-panel.tsx

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

+ 69 - 0
web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-trigger.tsx

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

+ 44 - 2
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx

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

+ 91 - 1
web/app/components/workflow/nodes/knowledge-retrieval/types.ts

@@ -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; }[]
 }

+ 140 - 3
web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts

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

+ 5 - 0
web/context/debug-configuration.ts

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

+ 19 - 19
web/hooks/use-metadata.ts

@@ -8,24 +8,24 @@ export type inputType = 'input' | 'select' | 'textarea'
 export type metadataType = DocType | 'originInfo' | 'technicalParameters'
 
 type MetadataMap =
-    Record<
-      metadataType,
-      {
-        text: string
-        allowEdit?: boolean
-        icon?: React.ReactNode
-        iconName?: string
-        subFieldsMap: Record<
-          string,
-          {
-            label: string
-            inputType?: inputType
-            field?: string
-            render?: (value: any, total?: number) => React.ReactNode | string
-          }
-        >
-      }
-    >
+  Record<
+    metadataType,
+    {
+      text: string
+      allowEdit?: boolean
+      icon?: React.ReactNode
+      iconName?: string
+      subFieldsMap: Record<
+        string,
+        {
+          label: string
+          inputType?: inputType
+          field?: string
+          render?: (value: any, total?: number) => React.ReactNode | string
+        }
+      >
+    }
+  >
 
 const fieldPrefix = 'datasetDocuments.metadata.field'
 
@@ -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}`),
         },
       },
     },

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
web/i18n/en-US/billing.ts


+ 48 - 0
web/i18n/en-US/dataset.ts

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

+ 30 - 0
web/i18n/en-US/workflow.ts

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

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
web/i18n/ja-JP/billing.ts


+ 1 - 0
web/i18n/zh-Hans/billing.ts

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

+ 48 - 0
web/i18n/zh-Hans/dataset.ts

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

+ 30 - 0
web/i18n/zh-Hans/workflow.ts

@@ -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: '图片',

+ 12 - 0
web/models/datasets.ts

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

+ 33 - 25
web/models/debug.ts

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

+ 0 - 0
web/service/knowledge/use-dateset.ts


+ 1 - 1
web/service/knowledge/use-document.ts

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

+ 146 - 0
web/service/knowledge/use-metadata.ts

@@ -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()
+    },
+  })
+}