diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index 77d12d9b9..163632fc9 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -691,7 +691,7 @@ const DynamicForm = { useImperativeHandle( ref, () => ({ - submit: form.handleSubmit, + submit: form.handleSubmit(onSubmit), getValues: form.getValues, reset: (values?: T) => { if (values) { diff --git a/web/src/components/edit-tag/index.tsx b/web/src/components/edit-tag/index.tsx index 1b7c7a802..ccf10b8ef 100644 --- a/web/src/components/edit-tag/index.tsx +++ b/web/src/components/edit-tag/index.tsx @@ -1,7 +1,7 @@ import { PlusOutlined } from '@ant-design/icons'; import React, { useEffect, useRef, useState } from 'react'; -import { X } from 'lucide-react'; +import { Trash2 } from 'lucide-react'; import { Button } from '../ui/button'; import { HoverCard, @@ -57,14 +57,15 @@ const EditTag = React.forwardRef( {tag} -
+
{tag}
{!disabled && ( - { e.preventDefault(); handleClose(tag); @@ -80,10 +81,6 @@ const EditTag = React.forwardRef( const tagChild = value?.map(forMap); - const tagPlusStyle: React.CSSProperties = { - borderStyle: 'dashed', - }; - return (
{inputVisible && ( @@ -102,15 +99,14 @@ const EditTag = React.forwardRef( }} /> )} -
+
{Array.isArray(tagChild) && tagChild.length > 0 && <>{tagChild}} {!inputVisible && !disabled && ( diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 8bee75987..9c353b367 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -175,6 +175,21 @@ Procedural Memory: Learned skills, habits, and automated procedures.`, parserRequired: 'Chunk method is required', }, knowledgeDetails: { + metadata: { + changesAffectNewParses: 'Changes affect new parses only.', + editMetadataForDataset: 'View and edit metadata for ', + restrictDefinedValues: 'Restrict to defined values', + metadataGenerationSettings: 'Metadata generation settings', + manageMetadataForDataset: 'Manage metadata for this dataset', + manageMetadata: 'Manage metadata', + metadata: 'Metadata', + values: 'Values', + action: 'Action', + field: 'Field', + description: 'Description', + fieldName: 'Field name', + editMetadata: 'Edit metadata', + }, localUpload: 'Local upload', fileSize: 'File size', fileType: 'File type', @@ -348,6 +363,8 @@ Procedural Memory: Learned skills, habits, and automated procedures.`, reRankModelWaring: 'Re-rank model is very time consuming.', }, knowledgeConfiguration: { + settings: 'Settings', + autoMetadata: 'Auto metadata', mineruOptions: 'MinerU Options', mineruParseMethod: 'Parse Method', mineruParseMethodTip: diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index cdce6076a..21791f872 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -267,6 +267,8 @@ export default { theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除', }, knowledgeConfiguration: { + settings: '设置', + autoMetadata: '自动元数据', mineruOptions: 'MinerU 选项', mineruParseMethod: '解析方法', mineruParseMethodTip: diff --git a/web/src/pages/dataset/components/metedata/hook.ts b/web/src/pages/dataset/components/metedata/hook.ts new file mode 100644 index 000000000..d2163cdaf --- /dev/null +++ b/web/src/pages/dataset/components/metedata/hook.ts @@ -0,0 +1,370 @@ +import message from '@/components/ui/message'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useSetDocumentMeta } from '@/hooks/use-document-request'; +import { + getMetaDataService, + updateMetaData, +} from '@/services/knowledge-service'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'umi'; +import { + IMetaDataReturnJSONSettings, + IMetaDataReturnJSONType, + IMetaDataReturnType, + IMetaDataTableData, + MetadataOperations, + ShowManageMetadataModalProps, +} from './interface'; +export enum MetadataType { + Manage = 1, + UpdateSingle = 2, + Setting = 3, +} +export const util = { + changeToMetaDataTableData(data: IMetaDataReturnType): IMetaDataTableData[] { + return Object.entries(data).map(([key, value]) => { + const values = value.map(([v]) => v.toString()); + console.log('values', values); + return { + field: key, + description: '', + values: values, + } as IMetaDataTableData; + }); + }, + + JSONToMetaDataTableData( + data: Record, + ): IMetaDataTableData[] { + return Object.entries(data).map(([key, value]) => { + return { + field: key, + description: '', + values: value, + } as IMetaDataTableData; + }); + }, + + tableDataToMetaDataJSON(data: IMetaDataTableData[]): IMetaDataReturnJSONType { + return data.reduce((pre, cur) => { + pre[cur.field] = cur.values; + return pre; + }, {}); + }, + + tableDataToMetaDataSettingJSON( + data: IMetaDataTableData[], + ): IMetaDataReturnJSONSettings { + return data.map((item) => { + return { + key: item.field, + description: item.description, + enum: item.values, + }; + }); + }, + + metaDataSettingJSONToMetaDataTableData( + data: IMetaDataReturnJSONSettings, + ): IMetaDataTableData[] { + return data.map((item) => { + return { + field: item.key, + description: item.description, + values: item.enum, + restrictDefinedValues: !!item.enum?.length, + } as IMetaDataTableData; + }); + }, +}; + +export const useMetadataOperations = () => { + const [operations, setOperations] = useState({ + deletes: [], + updates: [], + }); + + const addDeleteRow = useCallback((key: string) => { + setOperations((prev) => ({ + ...prev, + deletes: [...prev.deletes, { key }], + })); + }, []); + + const addDeleteValue = useCallback((key: string, value: string) => { + setOperations((prev) => ({ + ...prev, + deletes: [...prev.deletes, { key, value }], + })); + }, []); + + const addUpdateValue = useCallback( + (key: string, value: string | string[]) => { + setOperations((prev) => ({ + ...prev, + updates: [...prev.updates, { key, value }], + })); + }, + [], + ); + + const resetOperations = useCallback(() => { + setOperations({ + deletes: [], + updates: [], + }); + }, []); + + return { + operations, + addDeleteRow, + addDeleteValue, + addUpdateValue, + resetOperations, + }; +}; + +export const useFetchMetaDataManageData = ( + type: MetadataType = MetadataType.Manage, +) => { + const { id } = useParams(); + // const [data, setData] = useState([]); + // const [loading, setLoading] = useState(false); + // const fetchData = useCallback(async (): Promise => { + // setLoading(true); + // const { data } = await getMetaDataService({ + // kb_id: id as string, + // }); + // setLoading(false); + // if (data?.data?.summary) { + // return util.changeToMetaDataTableData(data.data.summary); + // } + // return []; + // }, [id]); + // useEffect(() => { + // if (type === MetadataType.Manage) { + // fetchData() + // .then((res) => { + // setData(res); + // }) + // .catch((res) => { + // console.error(res); + // }); + // } + // }, [type, fetchData]); + + const { + data, + isFetching: loading, + refetch, + } = useQuery({ + queryKey: ['fetchMetaData', id], + enabled: !!id && type === MetadataType.Manage, + initialData: [], + gcTime: 1000, + queryFn: async () => { + const { data } = await getMetaDataService({ + kb_id: id as string, + }); + if (data?.data?.summary) { + return util.changeToMetaDataTableData(data.data.summary); + } + return []; + }, + }); + return { + data, + loading, + refetch, + }; +}; + +export const useManageMetaDataModal = ( + metaData: IMetaDataTableData[] = [], + type: MetadataType = MetadataType.Manage, + otherData?: Record, +) => { + const { id } = useParams(); + const { t } = useTranslation(); + const { data, loading } = useFetchMetaDataManageData(type); + + const [tableData, setTableData] = useState(metaData); + + const { operations, addDeleteRow, addDeleteValue, addUpdateValue } = + useMetadataOperations(); + + const { setDocumentMeta } = useSetDocumentMeta(); + + useEffect(() => { + if (data) { + setTableData(data); + } else { + setTableData([]); + } + }, [data]); + + useEffect(() => { + if (metaData) { + setTableData(metaData); + } else { + setTableData([]); + } + }, [metaData]); + + const handleDeleteSingleValue = useCallback( + (field: string, value: string) => { + addDeleteValue(field, value); + + setTableData((prevTableData) => { + const newTableData = prevTableData.map((item) => { + if (item.field === field) { + return { + ...item, + values: item.values.filter((v) => v !== value), + }; + } + return item; + }); + // console.log('newTableData', newTableData, prevTableData); + return newTableData; + }); + }, + [addDeleteValue], + ); + + const handleDeleteSingleRow = useCallback( + (field: string) => { + addDeleteRow(field); + setTableData((prevTableData) => { + const newTableData = prevTableData.filter( + (item) => item.field !== field, + ); + // console.log('newTableData', newTableData, prevTableData); + return newTableData; + }); + }, + [addDeleteRow], + ); + + const handleSaveManage = useCallback( + async (callback: () => void) => { + const { data: res } = await updateMetaData({ + kb_id: id as string, + data: operations, + }); + if (res.code === 0) { + message.success(t('message.success')); + callback(); + } + }, + [operations, id, t], + ); + + const handleSaveUpdateSingle = useCallback( + async (callback: () => void) => { + const reqData = util.tableDataToMetaDataJSON(tableData); + if (otherData?.id) { + const ret = await setDocumentMeta({ + documentId: otherData?.id, + meta: JSON.stringify(reqData), + }); + if (ret === 0) { + // message.success(t('message.success')); + callback(); + } + } + }, + [tableData, otherData, setDocumentMeta], + ); + + const handleSaveSettings = useCallback( + async (callback: () => void) => { + const data = util.tableDataToMetaDataSettingJSON(tableData); + callback(); + + return data; + }, + [tableData], + ); + + const handleSave = useCallback( + async ({ callback }: { callback: () => void }) => { + switch (type) { + case MetadataType.UpdateSingle: + handleSaveUpdateSingle(callback); + break; + case MetadataType.Manage: + handleSaveManage(callback); + break; + case MetadataType.Setting: + return handleSaveSettings(callback); + default: + handleSaveManage(callback); + break; + } + }, + [handleSaveManage, type, handleSaveUpdateSingle, handleSaveSettings], + ); + + return { + tableData, + setTableData, + handleDeleteSingleValue, + handleDeleteSingleRow, + loading, + handleSave, + addUpdateValue, + addDeleteValue, + }; +}; + +export const useManageMetadata = () => { + const [tableData, setTableData] = useState([]); + const [config, setConfig] = useState( + {} as ShowManageMetadataModalProps, + ); + const { + visible: manageMetadataVisible, + showModal, + hideModal: hideManageMetadataModal, + } = useSetModalState(); + const showManageMetadataModal = useCallback( + (config?: ShowManageMetadataModalProps) => { + const { metadata } = config || {}; + if (metadata) { + // const dataTemp = Object.entries(metadata).map(([key, value]) => { + // return { + // field: key, + // description: '', + // values: Array.isArray(value) ? value : [value], + // } as IMetaDataTableData; + // }); + setTableData(metadata); + console.log('metadata-2', metadata); + } + console.log('metadata-3', metadata); + if (config) { + setConfig(config); + } + showModal(); + }, + [showModal], + ); + return { + manageMetadataVisible, + showManageMetadataModal, + hideManageMetadataModal, + tableData, + config, + }; +}; + +export const useManageValues = () => { + const [updateValues, setUpdateValues] = useState<{ + field: string; + values: string[]; + } | null>(null); + return { updateValues, setUpdateValues }; +}; diff --git a/web/src/pages/dataset/components/metedata/interface.ts b/web/src/pages/dataset/components/metedata/interface.ts new file mode 100644 index 000000000..5ba08f548 --- /dev/null +++ b/web/src/pages/dataset/components/metedata/interface.ts @@ -0,0 +1,80 @@ +import { ReactNode } from 'react'; +import { MetadataType } from './hook'; +export type IMetaDataReturnType = Record>>; +export type IMetaDataReturnJSONType = Record< + string, + Array | string +>; + +export interface IMetaDataReturnJSONSettingItem { + key: string; + description?: string; + enum?: string[]; +} +export type IMetaDataReturnJSONSettings = Array; + +export type IMetaDataTableData = { + field: string; + description: string; + restrictDefinedValues?: boolean; + values: string[]; +}; + +export type IManageModalProps = { + title: ReactNode; + isShowDescription?: boolean; + isDeleteSingleValue?: boolean; + visible: boolean; + hideModal: () => void; + tableData?: IMetaDataTableData[]; + isCanAdd: boolean; + type: MetadataType; + otherData?: Record; + isEditField?: boolean; + isAddValue?: boolean; + isShowValueSwitch?: boolean; + isVerticalShowValue?: boolean; + success?: (data: any) => void; +}; + +export interface IManageValuesProps { + title: ReactNode; + visible: boolean; + isEditField?: boolean; + isAddValue?: boolean; + isShowDescription?: boolean; + isShowValueSwitch?: boolean; + isVerticalShowValue?: boolean; + data: IMetaDataTableData; + hideModal: () => void; + onSave: (data: IMetaDataTableData) => void; + addUpdateValue: (key: string, value: string | string[]) => void; + addDeleteValue: (key: string, value: string) => void; +} + +interface DeleteOperation { + key: string; + value?: string; +} + +interface UpdateOperation { + key: string; + value: string | string[]; +} + +export interface MetadataOperations { + deletes: DeleteOperation[]; + updates: UpdateOperation[]; +} +export interface ShowManageMetadataModalOptions { + title?: ReactNode | string; +} +export type ShowManageMetadataModalProps = Partial & { + metadata?: IMetaDataTableData[]; + isCanAdd: boolean; + type: MetadataType; + record?: Record; + options?: ShowManageMetadataModalOptions; + title?: ReactNode | string; + isDeleteSingleValue?: boolean; +}; diff --git a/web/src/pages/dataset/components/metedata/manage-modal.tsx b/web/src/pages/dataset/components/metedata/manage-modal.tsx new file mode 100644 index 000000000..222235907 --- /dev/null +++ b/web/src/pages/dataset/components/metedata/manage-modal.tsx @@ -0,0 +1,373 @@ +import { + ConfirmDeleteDialog, + ConfirmDeleteDialogNode, +} from '@/components/confirm-delete-dialog'; +import { EmptyType } from '@/components/empty/constant'; +import Empty from '@/components/empty/empty'; +import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { Plus, Settings, Trash2 } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useManageMetaDataModal } from './hook'; +import { IManageModalProps, IMetaDataTableData } from './interface'; +import { ManageValuesModal } from './manage-values-modal'; +export const ManageMetadataModal = (props: IManageModalProps) => { + const { + title, + visible, + hideModal, + isDeleteSingleValue, + tableData: originalTableData, + isCanAdd, + type: metadataType, + otherData, + isEditField, + isAddValue, + isShowDescription = false, + isShowValueSwitch = false, + isVerticalShowValue = true, + success, + } = props; + const { t } = useTranslation(); + const [valueData, setValueData] = useState({ + field: '', + description: '', + values: [], + }); + + const [currentValueIndex, setCurrentValueIndex] = useState(0); + const [deleteDialogContent, setDeleteDialogContent] = useState({ + visible: false, + title: '', + name: '', + warnText: '', + onOk: () => {}, + onCancel: () => {}, + }); + + const { + tableData, + setTableData, + handleDeleteSingleValue, + handleDeleteSingleRow, + handleSave, + addUpdateValue, + addDeleteValue, + } = useManageMetaDataModal(originalTableData, metadataType, otherData); + + const { + visible: manageValuesVisible, + showModal: showManageValuesModal, + hideModal: hideManageValuesModal, + } = useSetModalState(); + const hideDeleteModal = () => { + setDeleteDialogContent({ + visible: false, + title: '', + name: '', + warnText: '', + onOk: () => {}, + onCancel: () => {}, + }); + }; + const handAddValueRow = () => { + setValueData({ + field: '', + description: '', + values: [], + }); + setCurrentValueIndex(tableData.length || 0); + showManageValuesModal(); + }; + const handleEditValueRow = useCallback( + (data: IMetaDataTableData, index: number) => { + setCurrentValueIndex(index); + setValueData(data); + showManageValuesModal(); + }, + [showManageValuesModal], + ); + + const columns: ColumnDef[] = useMemo(() => { + const cols: ColumnDef[] = [ + { + accessorKey: 'field', + header: () => {t('knowledgeDetails.metadata.field')}, + cell: ({ row }) => ( +
+ {row.getValue('field')} +
+ ), + }, + { + accessorKey: 'description', + header: () => {t('knowledgeDetails.metadata.description')}, + cell: ({ row }) => ( +
+ {row.getValue('description')} +
+ ), + }, + { + accessorKey: 'values', + header: () => {t('knowledgeDetails.metadata.values')}, + cell: ({ row }) => { + const values = row.getValue('values') as Array; + return ( +
+ {values.length > 0 && + values + .filter((value: string, index: number) => index < 2) + ?.map((value: string) => { + return ( + + )} +
+ + ); + })} + {values.length > 2 && ( +
...
+ )} +
+ ); + }, + }, + { + accessorKey: 'action', + header: () => {t('knowledgeDetails.metadata.action')}, + meta: { + cellClassName: 'w-12', + }, + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + if (!isShowDescription) { + cols.splice(1, 1); + } + return cols; + }, [ + handleDeleteSingleRow, + t, + handleDeleteSingleValue, + isShowDescription, + isDeleteSingleValue, + handleEditValueRow, + ]); + + const table = useReactTable({ + data: tableData as IMetaDataTableData[], + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + manualPagination: true, + }); + + const handleSaveValues = (data: IMetaDataTableData) => { + setTableData((prev) => { + if (currentValueIndex >= prev.length) { + return [...prev, data]; + } else { + return prev.map((item, index) => { + if (index === currentValueIndex) { + return data; + } + return item; + }); + } + }); + }; + + return ( + <> + { + const res = await handleSave({ callback: hideModal }); + console.log('data', res); + success?.(res); + }} + > +
+
+
{t('knowledgeDetails.metadata.metadata')}
+ {isCanAdd && ( + + )} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + + + + )} + +
+
+
+ {manageValuesVisible && ( + {t('knowledgeDetails.metadata.editMetadata')}
} + visible={manageValuesVisible} + hideModal={hideManageValuesModal} + data={valueData} + onSave={handleSaveValues} + addUpdateValue={addUpdateValue} + addDeleteValue={addDeleteValue} + isEditField={isEditField || isCanAdd} + isAddValue={isAddValue || isCanAdd} + isShowDescription={isShowDescription} + isShowValueSwitch={isShowValueSwitch} + isVerticalShowValue={isVerticalShowValue} + // handleDeleteSingleValue={handleDeleteSingleValue} + // handleDeleteSingleRow={handleDeleteSingleRow} + /> + )} + + {deleteDialogContent.visible && ( + + ), + }} + /> + )} + + ); +}; diff --git a/web/src/pages/dataset/components/metedata/manage-values-modal.tsx b/web/src/pages/dataset/components/metedata/manage-values-modal.tsx new file mode 100644 index 000000000..d10a22c0f --- /dev/null +++ b/web/src/pages/dataset/components/metedata/manage-values-modal.tsx @@ -0,0 +1,250 @@ +import EditTag from '@/components/edit-tag'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Modal } from '@/components/ui/modal/modal'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import { Plus, Trash2 } from 'lucide-react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IManageValuesProps, IMetaDataTableData } from './interface'; + +// Create a separate input component, wrapped with memo to avoid unnecessary re-renders +const ValueInputItem = memo( + ({ + item, + index, + onValueChange, + onDelete, + onBlur, + }: { + item: string; + index: number; + onValueChange: (index: number, value: string) => void; + onDelete: (index: number) => void; + onBlur: (index: number) => void; + }) => { + return ( +
+
+ onValueChange(index, e.target.value)} + onBlur={() => onBlur(index)} + /> +
+ +
+ ); + }, +); + +export const ManageValuesModal = (props: IManageValuesProps) => { + const { + title, + data, + isEditField, + visible, + isAddValue, + isShowDescription, + isShowValueSwitch, + isVerticalShowValue, + hideModal, + onSave, + addUpdateValue, + addDeleteValue, + } = props; + const [metaData, setMetaData] = useState(data); + const { t } = useTranslation(); + + // Use functional update to avoid closure issues + const handleChange = useCallback((field: string, value: any) => { + setMetaData((prev) => ({ + ...prev, + [field]: value, + })); + }, []); + + // Maintain separate state for each input box + const [tempValues, setTempValues] = useState([...data.values]); + + useEffect(() => { + setTempValues([...data.values]); + setMetaData(data); + }, [data]); + + const handleHideModal = useCallback(() => { + hideModal(); + setMetaData({} as IMetaDataTableData); + }, [hideModal]); + + const handleSave = useCallback(() => { + if (!metaData.restrictDefinedValues && isShowValueSwitch) { + const newMetaData = { ...metaData, values: [] }; + onSave(newMetaData); + } else { + onSave(metaData); + } + handleHideModal(); + }, [metaData, onSave, handleHideModal, isShowValueSwitch]); + + // Handle value changes, only update temporary state + const handleValueChange = useCallback((index: number, value: string) => { + setTempValues((prev) => { + const newValues = [...prev]; + newValues[index] = value; + + return newValues; + }); + }, []); + + // Handle blur event, synchronize to main state + const handleValueBlur = useCallback(() => { + addUpdateValue(metaData.field, [...tempValues]); + handleChange('values', [...tempValues]); + }, [handleChange, tempValues, metaData, addUpdateValue]); + + // Handle delete operation + const handleDelete = useCallback( + (index: number) => { + setTempValues((prev) => { + const newTempValues = [...prev]; + addDeleteValue(metaData.field, newTempValues[index]); + newTempValues.splice(index, 1); + return newTempValues; + }); + + // Synchronize to main state + setMetaData((prev) => { + const newMetaDataValues = [...prev.values]; + newMetaDataValues.splice(index, 1); + return { + ...prev, + values: newMetaDataValues, + }; + }); + }, + [addDeleteValue, metaData], + ); + + // Handle adding new value + const handleAddValue = useCallback(() => { + setTempValues((prev) => [...prev, '']); + + // Synchronize to main state + setMetaData((prev) => ({ + ...prev, + values: [...prev.values, ''], + })); + }, []); + + return ( + +
+ {!isEditField && ( +
+ {metaData.field} +
+ )} + {isEditField && ( +
+
{t('knowledgeDetails.metadata.fieldName')}
+
+ { + handleChange('field', e.target?.value || ''); + }} + /> +
+
+ )} + {isShowDescription && ( +
+
{t('knowledgeDetails.metadata.description')}
+
+