diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx index c276f55a23..7d32f8cebe 100644 --- a/web/app/(commonLayout)/datasets/template/template.en.mdx +++ b/web/app/(commonLayout)/datasets/template/template.en.mdx @@ -1543,6 +1543,255 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
+ + + + ### Params + + + Knowledge ID + + + + ### Request Body + + + - type (string) Metadata type, required + - name (string) Metadata name, required + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "id": "abc", + "type": "string", + "name": "test", + } + ``` + + + + +
+ + + + + ### Params + + + Knowledge ID + + + Metadata ID + + + + ### Request Body + + + - name (string) Metadata name, required + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "id": "abc", + "type": "string", + "name": "test", + } + ``` + + + + +
+ + + + + ### Params + + + Knowledge ID + + + Metadata ID + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Params + + + Knowledge ID + + + disable/enable + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Params + + + Knowledge ID + + + + ### Request Body + + + - document_id (string) Document ID + - metadata_list (list) Metadata list + - id (string) Metadata ID + - value (string) Metadata value + - name (string) Metadata name + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Params + + + Knowledge ID + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "doc_metadata": [ + { + "id": "", + "name": "name", + "type": "string", + "use_count": 0, + }, + ... + ], + "built_in_field_enabled": true + } + ``` + + + + +
+ ### Error message diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index db72ef9a08..8bd3d8d5eb 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -1547,6 +1547,254 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi +
+ + + + + ### Params + + + 知识库 ID + + + + ### Request Body + + + - type (string) 元数据类型,必填 + - name (string) 元数据名称,必填 + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "id": "abc", + "type": "string", + "name": "test", + } + ``` + + + + +
+ + + + + ### Params + + + 知识库 ID + + + 元数据 ID + + + + ### Request Body + + + - name (string) 元数据名称,必填 + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "id": "abc", + "type": "string", + "name": "test", + } + ``` + + + + +
+ + + + + ### Params + + + 知识库 ID + + + 元数据 ID + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Params + + + 知识库 ID + + + disable/enable + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Params + + + 知识库 ID + + + + ### Request Body + + + - document_id (string) 文档 ID + - metadata_list (list) 元数据列表 + - id (string) 元数据 ID + - type (string) 元数据类型 + - name (string) 元数据名称 + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + + +
+ + + + + ### Query + + + 知识库 ID + + + + + + ```bash {{ title: 'cURL' }} + ``` + + + ```json {{ title: 'Response' }} + { + "doc_metadata": [ + { + "id": "", + "name": "name", + "type": "string", + "use_count": 0, + }, + ... + ], + "built_in_field_enabled": true + } + ``` + + +
diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index 5868118e94..b1de2cabd9 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/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(({ 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((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((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(() => { + 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) => { + const newInputs = produce(datasetConfigsRef.current!, (draft) => { + draft.metadata_model_config = { + ...draft.metadata_model_config!, + completion_params: newParams, + } + }) + setDatasetConfigs(newInputs) + }, [setDatasetConfigs, datasetConfigsRef]) + return ( { )} +
+ item.type === MetadataFilteringVariableType.string)} + availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)} + /> +
+ {mode === AppType.completion && dataSet.length > 0 && ( { 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({ + const [datasetConfigs, doSetDatasetConfigs] = useState({ 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, diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx index 82f6500f1d..ed0cd80784 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx +++ b/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' > - + {renderTrigger ? (renderTrigger({ value, selectedDate, @@ -234,7 +238,7 @@ const DatePicker = ({ )} - +
{/* Header */} {view === ViewType.date ? ( diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 56e0ef6d50..214c0f011b 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/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 = { diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx index e34dc7697a..f12aa27b24 100644 --- a/web/app/components/base/drawer/index.tsx +++ b/web/app/components/base/drawer/index.tsx @@ -53,15 +53,17 @@ export default function Drawer({ />
<> - {title && - {title} - } - {showClose && - - } +
+ {title && + {title} + } + {showClose && + + } +
{description && {description}} {children} diff --git a/web/app/components/base/input-number/index.tsx b/web/app/components/base/input-number/index.tsx index 71356620f0..5b88fc67f8 100644 --- a/web/app/components/base/input-number/index.tsx +++ b/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 export const InputNumber: FC = (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 = (props) => { onChange(newValue) } - return
+ return
= (props) => {
-
diff --git a/web/app/components/base/modal-like-wrap/index.tsx b/web/app/components/base/modal-like-wrap/index.tsx new file mode 100644 index 0000000000..2eba382082 --- /dev/null +++ b/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 = ({ + title, + className, + beforeHeader, + children, + onClose, + hideCloseBtn, + onConfirm, +}) => { + const { t } = useTranslation() + + return ( +
+ {beforeHeader || null} +
+
{title}
+ {!hideCloseBtn && ( +
+ +
+ )} +
+
{children}
+
+ + +
+
+ ) +} + +export default React.memo(ModalLikeWrap) diff --git a/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx b/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx index a8da9bf6cc..171e99e72e 100644 --- a/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx +++ b/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 = ({ const { Icon, color } = getIcon(type) return (
-
{description}
- -
{actionText}
+ {onAction && ( + <> + +
{actionText}
+ + )}
) diff --git a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx index 3dd3689b64..040b4faded 100644 --- a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx +++ b/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 onArchive?: () => void + onEditMetadata?: () => void onCancel: () => void } @@ -24,6 +25,7 @@ const BatchAction: FC = ({ onBatchDisable, onArchive, onBatchDelete, + onEditMetadata, onCancel, }) => { const { t } = useTranslation() @@ -62,6 +64,15 @@ const BatchAction: FC = ({ {t(`${i18nPrefix}.disable`)}
+ {onEditMetadata && ( +
+ + +
+ )} + {onArchive && (
diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 6f5e84971e..fb44f9dbea 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/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 = ({ datasetId, documentId }) => { } setShowMetadata(false)} isMobile={isMobile} panelClassname='!justify-start' footer={null}>
diff --git a/web/app/components/datasets/documents/index.tsx b/web/app/components/datasets/documents/index.tsx index b28701d668..8c2bb32887 100644 --- a/web/app/components/datasets/documents/index.tsx +++ b/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) => { return @@ -116,7 +119,7 @@ const Documents: FC = ({ 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 = ({ datasetId }) => { handleSearch() } + const { + isShowEditModal: isShowEditMetadataModal, + showEditModal: showEditMetadataModal, + hideEditModal: hideEditMetadataModal, + datasetMetaData, + handleAddMetaData, + handleRename, + handleDeleteMetaData, + builtInEnabled, + setBuiltInEnabled, + builtInMetaData, + } = useEditDocumentMetadata({ + datasetId, + dataset, + onUpdateDocList: invalidDocumentList, + }) + return (
@@ -259,6 +279,25 @@ const Documents: FC = ({ datasetId }) => {
{!isFreePlan && } + {!embeddingAvailable && } + {embeddingAvailable && ( + + )} + {isShowEditMetadataModal && ( + + )} {embeddingAvailable && (