Selaa lähdekoodia

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 1 kuukausi sitten
vanhempi
commit
20376ca951
72 muutettua tiedostoa jossa 4775 lisäystä ja 101 poistoa
  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

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

+ 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

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

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