Feat: Save document metadata #3221 (#7323)

### What problem does this PR solve?

Feat: Save document metadata #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-04-25 18:38:15 +08:00
committed by GitHub
parent 1662c7eda3
commit 3052006ba8
8 changed files with 282 additions and 29 deletions

View File

@ -1,4 +1,3 @@
import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -45,6 +44,7 @@ import RaptorFormFields, {
showRaptorParseConfiguration, showRaptorParseConfiguration,
} from '../parse-configuration/raptor-form-fields'; } from '../parse-configuration/raptor-form-fields';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { LoadingButton } from '../ui/loading-button';
import { RAGFlowSelect } from '../ui/select'; import { RAGFlowSelect } from '../ui/select';
import { DynamicPageRange } from './dynamic-page-range'; import { DynamicPageRange } from './dynamic-page-range';
import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks'; import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks';
@ -84,6 +84,7 @@ export function ChunkMethodDialog({
documentExtension, documentExtension,
visible, visible,
parserConfig, parserConfig,
loading,
}: IProps) { }: IProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -108,34 +109,37 @@ export function ChunkMethodDialog({
parser_id: z parser_id: z
.string() .string()
.min(1, { .min(1, {
message: 'namePlaceholder', message: t('common.pleaseSelect'),
}) })
.trim(), .trim(),
parser_config: z.object({ parser_config: z.object({
task_page_size: z.coerce.number(), task_page_size: z.coerce.number().optional(),
layout_recognize: z.string(), layout_recognize: z.string().optional(),
chunk_token_num: z.coerce.number(), chunk_token_num: z.coerce.number().optional(),
delimiter: z.string(), delimiter: z.string().optional(),
auto_keywords: z.coerce.number(), auto_keywords: z.coerce.number().optional(),
auto_questions: z.coerce.number(), auto_questions: z.coerce.number().optional(),
html4excel: z.boolean(), html4excel: z.boolean().optional(),
raptor: z.object({ raptor: z
use_raptor: z.boolean().optional(), .object({
prompt: z.string(), use_raptor: z.boolean().optional(),
max_token: z.coerce.number(), prompt: z.string().optional().optional(),
threshold: z.coerce.number(), max_token: z.coerce.number().optional(),
max_cluster: z.coerce.number(), threshold: z.coerce.number().optional(),
random_seed: z.coerce.number(), max_cluster: z.coerce.number().optional(),
}), random_seed: z.coerce.number().optional(),
})
.optional(),
graphrag: z.object({ graphrag: z.object({
use_graphrag: z.boolean(), use_graphrag: z.boolean().optional(),
}), }),
entity_types: z.array(z.string()), entity_types: z.array(z.string()).optional(),
pages: z.array( pages: z
z.object({ from: z.coerce.number(), to: z.coerce.number() }), .array(z.object({ from: z.coerce.number(), to: z.coerce.number() }))
), .optional(),
}), }),
}); });
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { defaultValues: {
@ -316,9 +320,9 @@ export function ChunkMethodDialog({
</form> </form>
</Form> </Form>
<DialogFooter> <DialogFooter>
<Button type="submit" form={FormId}> <LoadingButton type="submit" form={FormId} loading={loading}>
{t('common.save')} {t('common.save')}
</Button> </LoadingButton>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,5 +1,8 @@
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; import {
IChangeParserConfigRequestBody,
IDocumentMetaRequestBody,
} from '@/interfaces/request/document';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import kbService from '@/services/knowledge-service'; import kbService from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@ -22,6 +25,7 @@ export const enum DocumentApiAction {
RemoveDocument = 'removeDocument', RemoveDocument = 'removeDocument',
SaveDocumentName = 'saveDocumentName', SaveDocumentName = 'saveDocumentName',
SetDocumentParser = 'setDocumentParser', SetDocumentParser = 'setDocumentParser',
SetDocumentMeta = 'setDocumentMeta',
} }
export const useUploadNextDocument = () => { export const useUploadNextDocument = () => {
@ -286,3 +290,36 @@ export const useSetDocumentParser = () => {
return { setDocumentParser: mutateAsync, data, loading }; return { setDocumentParser: mutateAsync, data, loading };
}; };
export const useSetDocumentMeta = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SetDocumentMeta],
mutationFn: async (params: IDocumentMetaRequestBody) => {
try {
const { data } = await kbService.setMeta({
meta: params.meta,
doc_id: params.documentId,
});
if (data?.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.modified'));
}
return data?.code;
} catch (error) {
message.error('error');
}
},
});
return { setDocumentMeta: mutateAsync, data, loading };
};

View File

@ -29,9 +29,11 @@ import { useFetchDocumentList } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
import { getExtension } from '@/utils/document-util'; import { getExtension } from '@/utils/document-util';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SetMetaDialog } from './set-meta-dialog';
import { useChangeDocumentParser } from './use-change-document-parser'; import { useChangeDocumentParser } from './use-change-document-parser';
import { useDatasetTableColumns } from './use-dataset-table-columns'; import { useDatasetTableColumns } from './use-dataset-table-columns';
import { useRenameDocument } from './use-rename-document'; import { useRenameDocument } from './use-rename-document';
import { useSaveMeta } from './use-save-meta';
export function DatasetTable() { export function DatasetTable() {
const { const {
@ -69,10 +71,20 @@ export function DatasetTable() {
initialName, initialName,
} = useRenameDocument(); } = useRenameDocument();
const {
showSetMetaModal,
hideSetMetaModal,
setMetaVisible,
setMetaLoading,
onSetMetaModalOk,
metaRecord,
} = useSaveMeta();
const columns = useDatasetTableColumns({ const columns = useDatasetTableColumns({
showChangeParserModal, showChangeParserModal,
setCurrentRecord: setRecord, setCurrentRecord: setRecord,
showRenameModal, showRenameModal,
showSetMetaModal,
}); });
const currentPagination = useMemo(() => { const currentPagination = useMemo(() => {
@ -219,6 +231,15 @@ export function DatasetTable() {
initialName={initialName} initialName={initialName}
></RenameDialog> ></RenameDialog>
)} )}
{setMetaVisible && (
<SetMetaDialog
hideModal={hideSetMetaModal}
loading={setMetaLoading}
onOk={onSetMetaModalOk}
initialMetaData={metaRecord.meta_fields}
></SetMetaDialog>
)}
</div> </div>
); );
} }

View File

@ -16,6 +16,7 @@ import { RunningStatus } from './constant';
import { ParsingCard } from './parsing-card'; import { ParsingCard } from './parsing-card';
import { UseChangeDocumentParserShowType } from './use-change-document-parser'; import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { useHandleRunDocumentByIds } from './use-run-document'; import { useHandleRunDocumentByIds } from './use-run-document';
import { UseSaveMetaShowType } from './use-save-meta';
import { isParserRunning } from './utils'; import { isParserRunning } from './utils';
const IconMap = { const IconMap = {
@ -29,7 +30,9 @@ const IconMap = {
export function ParsingStatusCell({ export function ParsingStatusCell({
record, record,
showChangeParserModal, showChangeParserModal,
}: { record: IDocumentInfo } & UseChangeDocumentParserShowType) { showSetMetaModal,
}: { record: IDocumentInfo } & UseChangeDocumentParserShowType &
UseSaveMetaShowType) {
const { t } = useTranslation(); const { t } = useTranslation();
const { run, parser_id, progress, chunk_num, id } = record; const { run, parser_id, progress, chunk_num, id } = record;
const operationIcon = IconMap[run]; const operationIcon = IconMap[run];
@ -48,6 +51,10 @@ export function ParsingStatusCell({
showChangeParserModal(record); showChangeParserModal(record);
}, [record, showChangeParserModal]); }, [record, showChangeParserModal]);
const handleShowSetMetaModal = useCallback(() => {
showSetMetaModal(record);
}, [record, showSetMetaModal]);
return ( return (
<section className="flex gap-2 items-center "> <section className="flex gap-2 items-center ">
<div> <div>
@ -61,7 +68,7 @@ export function ParsingStatusCell({
<DropdownMenuItem onClick={handleShowChangeParserModal}> <DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.chunkMethod')} {t('knowledgeDetails.chunkMethod')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem onClick={handleShowSetMetaModal}>
{t('knowledgeDetails.setMetaData')} {t('knowledgeDetails.setMetaData')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -0,0 +1,128 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { IDocumentInfo } from '@/interfaces/database/document';
import Editor, { loader } from '@monaco-editor/react';
import DOMPurify from 'dompurify';
import { useEffect } from 'react';
loader.config({ paths: { vs: '/vs' } });
export function SetMetaDialog({
hideModal,
onOk,
loading,
initialMetaData,
}: IModalProps<any> & { initialMetaData?: IDocumentInfo['meta_fields'] }) {
const { t } = useTranslation();
const FormSchema = z.object({
meta: z
.string()
.min(1, {
message: t('knowledgeDetails.pleaseInputJson'),
})
.trim()
.refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{ message: t('knowledgeDetails.pleaseInputJson') },
),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {},
});
async function onSubmit(data: z.infer<typeof FormSchema>) {
const ret = await onOk?.(data.meta);
if (ret) {
hideModal?.();
}
}
useEffect(() => {
form.setValue('meta', JSON.stringify(initialMetaData, null, 4));
}, [form, initialMetaData]);
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('knowledgeDetails.setMetaData')}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id={TagRenameId}
>
<FormField
control={form.control}
name="meta"
render={({ field }) => (
<FormItem>
<FormLabel
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('knowledgeDetails.documentMetaTips'),
),
}}
></div>
}
>
{t('knowledgeDetails.metaData')}
</FormLabel>
<FormControl>
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -20,15 +20,18 @@ import { DatasetActionCell } from './dataset-action-cell';
import { ParsingStatusCell } from './parsing-status-cell'; import { ParsingStatusCell } from './parsing-status-cell';
import { UseChangeDocumentParserShowType } from './use-change-document-parser'; import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { UseRenameDocumentShowType } from './use-rename-document'; import { UseRenameDocumentShowType } from './use-rename-document';
import { UseSaveMetaShowType } from './use-save-meta';
type UseDatasetTableColumnsType = UseChangeDocumentParserShowType & { type UseDatasetTableColumnsType = UseChangeDocumentParserShowType & {
setCurrentRecord: (record: IDocumentInfo) => void; setCurrentRecord: (record: IDocumentInfo) => void;
} & UseRenameDocumentShowType; } & UseRenameDocumentShowType &
UseSaveMetaShowType;
export function useDatasetTableColumns({ export function useDatasetTableColumns({
showChangeParserModal, showChangeParserModal,
setCurrentRecord, setCurrentRecord,
showRenameModal, showRenameModal,
showSetMetaModal,
}: UseDatasetTableColumnsType) { }: UseDatasetTableColumnsType) {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails', keyPrefix: 'knowledgeDetails',
@ -161,6 +164,7 @@ export function useDatasetTableColumns({
<ParsingStatusCell <ParsingStatusCell
record={row.original} record={row.original}
showChangeParserModal={showChangeParserModal} showChangeParserModal={showChangeParserModal}
showSetMetaModal={showSetMetaModal}
></ParsingStatusCell> ></ParsingStatusCell>
); );
}, },

View File

@ -0,0 +1,50 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetDocumentMeta } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { useCallback, useState } from 'react';
export const useSaveMeta = () => {
const { setDocumentMeta, loading } = useSetDocumentMeta();
const [record, setRecord] = useState<IDocumentInfo>({} as IDocumentInfo);
const {
visible: setMetaVisible,
hideModal: hideSetMetaModal,
showModal: showSetMetaModal,
} = useSetModalState();
const onSetMetaModalOk = useCallback(
async (meta: string) => {
const ret = await setDocumentMeta({
documentId: record?.id,
meta,
});
if (ret === 0) {
hideSetMetaModal();
}
},
[setDocumentMeta, record?.id, hideSetMetaModal],
);
const handleShowSetMetaModal = useCallback(
(row: IDocumentInfo) => {
setRecord(row);
showSetMetaModal();
},
[showSetMetaModal],
);
return {
setMetaLoading: loading,
onSetMetaModalOk,
setMetaVisible,
hideSetMetaModal,
showSetMetaModal: handleShowSetMetaModal,
metaRecord: record,
};
};
export type UseSaveMetaShowType = Pick<
ReturnType<typeof useSaveMeta>,
'showSetMetaModal'
>;

View File

@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook'; import { useSecondPathName } from '@/hooks/route-hook';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { Banknote, LayoutGrid, User } from 'lucide-react'; import { Banknote, LayoutGrid, User } from 'lucide-react';
@ -27,6 +28,7 @@ const dataset = {
export function SideBar() { export function SideBar() {
const pathName = useSecondPathName(); const pathName = useSecondPathName();
const { handleMenuClick } = useHandleMenuClick(); const { handleMenuClick } = useHandleMenuClick();
const { data } = useFetchKnowledgeBaseConfiguration();
return ( return (
<aside className="w-[303px] relative border-r "> <aside className="w-[303px] relative border-r ">
@ -36,7 +38,7 @@ export function SideBar() {
style={{ backgroundImage: `url(${dataset.image})` }} style={{ backgroundImage: `url(${dataset.image})` }}
/> />
<h3 className="text-lg font-semibold mb-2">{dataset.title}</h3> <h3 className="text-lg font-semibold mb-2">{data.name}</h3>
<div className="text-sm opacity-80"> <div className="text-sm opacity-80">
{dataset.files} | {dataset.size} {dataset.files} | {dataset.size}
</div> </div>