From 1d93eb81ae2a754d2810707e30342f7e47ceed23 Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 6 Jan 2025 18:58:42 +0800 Subject: [PATCH] Feat: Add TagTable #4367 (#4368) ### What problem does this PR solve? Feat: Add TagTable #4367 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/auto-keywords-item.tsx | 12 +- .../components/chunk-method-modal/hooks.ts | 2 +- web/src/components/confirm-delete-dialog.tsx | 6 +- web/src/components/max-token-number.tsx | 1 - web/src/components/page-rank.tsx | 7 +- .../components/parse-configuration/index.tsx | 2 + web/src/hooks/knowledge-hooks.ts | 83 ++++- web/src/interfaces/database/knowledge.ts | 2 + web/src/locales/en.ts | 5 + web/src/locales/zh-traditional.ts | 5 + web/src/locales/zh.ts | 5 + .../knowledge-setting/category-panel.tsx | 2 + .../components/knowledge-setting/hooks.ts | 45 ++- .../knowledge-setting/tag-table/index.tsx | 309 ++++++++++++++++++ .../tag-table/rename-dialog/index.tsx | 38 +++ .../tag-table/rename-dialog/rename-form.tsx | 83 +++++ web/src/pages/add-knowledge/constant.ts | 2 + web/src/services/knowledge-service.ts | 14 +- web/src/utils/api.ts | 6 + 19 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 web/src/pages/add-knowledge/components/knowledge-setting/tag-table/index.tsx create mode 100644 web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/index.tsx create mode 100644 web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/rename-form.tsx diff --git a/web/src/components/auto-keywords-item.tsx b/web/src/components/auto-keywords-item.tsx index 61ea50469..a18c161e4 100644 --- a/web/src/components/auto-keywords-item.tsx +++ b/web/src/components/auto-keywords-item.tsx @@ -16,11 +16,7 @@ export const AutoKeywordsItem = () => { - + @@ -43,11 +39,7 @@ export const AutoQuestionsItem = () => { - + diff --git a/web/src/components/chunk-method-modal/hooks.ts b/web/src/components/chunk-method-modal/hooks.ts index 28d161126..72e7f89cc 100644 --- a/web/src/components/chunk-method-modal/hooks.ts +++ b/web/src/components/chunk-method-modal/hooks.ts @@ -119,7 +119,7 @@ export const useFetchParserListOnMount = ( return { parserList: nextParserList, handleChange, selectedTag }; }; -const hideAutoKeywords = ['qa', 'table', 'resume', 'knowledge_graph']; +const hideAutoKeywords = ['qa', 'table', 'resume', 'knowledge_graph', 'tag']; export const useShowAutoKeywords = () => { const showAutoKeywords = useCallback((selectedTag: string) => { diff --git a/web/src/components/confirm-delete-dialog.tsx b/web/src/components/confirm-delete-dialog.tsx index 887a1222c..3843b02a4 100644 --- a/web/src/components/confirm-delete-dialog.tsx +++ b/web/src/components/confirm-delete-dialog.tsx @@ -21,6 +21,7 @@ interface IProps { export function ConfirmDeleteDialog({ children, title, + onOk, }: IProps & PropsWithChildren) { const { t } = useTranslation(); @@ -39,7 +40,10 @@ export function ConfirmDeleteDialog({ {t('common.cancel')} - + {t('common.ok')} diff --git a/web/src/components/max-token-number.tsx b/web/src/components/max-token-number.tsx index 7cbb0204c..94829efd9 100644 --- a/web/src/components/max-token-number.tsx +++ b/web/src/components/max-token-number.tsx @@ -25,7 +25,6 @@ const MaxTokenNumber = ({ initialValue = 128, max = 2048 }: IProps) => { diff --git a/web/src/components/page-rank.tsx b/web/src/components/page-rank.tsx index cd60a87fd..24579d759 100644 --- a/web/src/components/page-rank.tsx +++ b/web/src/components/page-rank.tsx @@ -17,12 +17,7 @@ const PageRank = () => { - + diff --git a/web/src/components/parse-configuration/index.tsx b/web/src/components/parse-configuration/index.tsx index f7bb84378..7786eddcb 100644 --- a/web/src/components/parse-configuration/index.tsx +++ b/web/src/components/parse-configuration/index.tsx @@ -18,6 +18,8 @@ export const excludedParseMethods = [ 'one', 'picture', 'knowledge_graph', + 'qa', + 'tag', ]; export const showRaptorParseConfiguration = (parserId: string) => { diff --git a/web/src/hooks/knowledge-hooks.ts b/web/src/hooks/knowledge-hooks.ts index 23ca80e9f..545cb61c8 100644 --- a/web/src/hooks/knowledge-hooks.ts +++ b/web/src/hooks/knowledge-hooks.ts @@ -1,7 +1,15 @@ import { ResponsePostType } from '@/interfaces/database/base'; -import { IKnowledge, ITestingResult } from '@/interfaces/database/knowledge'; +import { + IKnowledge, + IRenameTag, + ITestingResult, +} from '@/interfaces/database/knowledge'; import i18n from '@/locales/config'; -import kbService from '@/services/knowledge-service'; +import kbService, { + listTag, + removeTag, + renameTag, +} from '@/services/knowledge-service'; import { useInfiniteQuery, useIsMutating, @@ -259,3 +267,74 @@ export const useSelectIsTestingSuccess = () => { return status.at(-1) === 'success'; }; //#endregion + +//#region tags + +export const useFetchTagList = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery>({ + queryKey: ['fetchTagList'], + initialData: [], + gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3 + queryFn: async () => { + const { data } = await listTag(knowledgeBaseId); + const list = data?.data || []; + return list; + }, + }); + + return { list: data, loading }; +}; + +export const useDeleteTag = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + + const queryClient = useQueryClient(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: ['deleteTag'], + mutationFn: async (tags: string[]) => { + const { data } = await removeTag(knowledgeBaseId, tags); + if (data.code === 0) { + message.success(i18n.t(`message.deleted`)); + queryClient.invalidateQueries({ + queryKey: ['fetchTagList'], + }); + } + return data?.data ?? []; + }, + }); + + return { data, loading, deleteTag: mutateAsync }; +}; + +export const useRenameTag = () => { + const knowledgeBaseId = useKnowledgeBaseId(); + + const queryClient = useQueryClient(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: ['deleteTag'], + mutationFn: async (params: IRenameTag) => { + const { data } = await renameTag(knowledgeBaseId, params); + if (data.code === 0) { + message.success(i18n.t(`message.modified`)); + queryClient.invalidateQueries({ + queryKey: ['fetchTagList'], + }); + } + return data?.data ?? []; + }, + }); + + return { data, loading, renameTag: mutateAsync }; +}; + +//#endregion diff --git a/web/src/interfaces/database/knowledge.ts b/web/src/interfaces/database/knowledge.ts index 82e84c06f..10a06774f 100644 --- a/web/src/interfaces/database/knowledge.ts +++ b/web/src/interfaces/database/knowledge.ts @@ -117,3 +117,5 @@ export interface ITestingResult { documents: ITestingDocument[]; total: number; } + +export type IRenameTag = { fromTag: string; toTag: string }; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index edf012a49..5d4eac990 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -34,6 +34,8 @@ export default { pleaseInput: 'Please input', submit: 'Submit', embedIntoSite: 'Embed into webpage', + previousPage: 'Previous', + nextPage: 'Next', }, login: { login: 'Sign in', @@ -308,6 +310,9 @@ The above is the content you need to summarize.`, vietnamese: 'Vietnamese', pageRank: 'Page rank', pageRankTip: `This increases the relevance score of the knowledge base. Its value will be added to the relevance score of all retrieved chunks from this knowledge base. Useful when you are searching within multiple knowledge bases and wanting to assign a higher pagerank score to a specific one.`, + tag: 'Tag', + frequency: 'Frequency', + searchTags: 'Search tags', }, chunk: { chunk: 'Chunk', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 75665e450..50a59c7dc 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -34,6 +34,8 @@ export default { pleaseInput: '請輸入', submit: '提交', embedIntoSite: '嵌入網站', + previousPage: '上一頁', + nextPage: '下一頁', }, login: { login: '登入', @@ -292,6 +294,9 @@ export default { pageRank: '頁面排名', pageRankTip: `這用來提高相關性分數。所有檢索到的區塊的相關性得分將加上該數字。 當您想要先搜尋給定的知識庫時,請設定比其他人更高的 pagerank 分數。`, + tag: '標籤', + frequency: '頻次', + searchTags: '搜尋標籤', }, chunk: { chunk: '解析塊', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index f3506bab8..2d6dc32c6 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -34,6 +34,8 @@ export default { pleaseInput: '请输入', submit: '提交', embedIntoSite: '嵌入网站', + previousPage: '上一页', + nextPage: '下一页', }, login: { login: '登录', @@ -309,6 +311,9 @@ export default { pageRank: '页面排名', pageRankTip: `这用于提高相关性得分。所有检索到的块的相关性得分将加上此数字。 当您想首先搜索给定的知识库时,请设置比其他知识库更高的 pagerank 得分。`, + tag: '标签', + frequency: '频次', + searchTags: '搜索标签', }, chunk: { chunk: '解析块', diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx index ba58003c6..78a73804b 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx @@ -6,6 +6,7 @@ import DOMPurify from 'dompurify'; import camelCase from 'lodash/camelCase'; import { useMemo } from 'react'; import styles from './index.less'; +import { TagTable } from './tag-table'; import { ImageMap } from './utils'; const { Title, Text } = Typography; @@ -68,6 +69,7 @@ const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => { )} + {chunkMethod === 'tag' && } ); }; diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts index 6f79a0819..205740d47 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts +++ b/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts @@ -1,6 +1,8 @@ import { LlmModelType } from '@/constants/knowledge'; +import { useSetModalState } from '@/hooks/common-hooks'; import { useFetchKnowledgeBaseConfiguration, + useRenameTag, useUpdateKnowledge, } from '@/hooks/knowledge-hooks'; import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks'; @@ -14,7 +16,7 @@ import { useIsFetching } from '@tanstack/react-query'; import { Form, UploadFile } from 'antd'; import { FormInstance } from 'antd/lib'; import pick from 'lodash/pick'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; export const useSubmitKnowledgeConfiguration = (form: FormInstance) => { const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge(); @@ -87,3 +89,44 @@ export const useHandleChunkMethodChange = () => { return { form, chunkMethod }; }; + +export const useRenameKnowledgeTag = () => { + const [tag, setTag] = useState(''); + const { + visible: tagRenameVisible, + hideModal: hideTagRenameModal, + showModal: showFileRenameModal, + } = useSetModalState(); + const { renameTag, loading } = useRenameTag(); + + const onTagRenameOk = useCallback( + async (name: string) => { + const ret = await renameTag({ + fromTag: tag, + toTag: name, + }); + + if (ret === 0) { + hideTagRenameModal(); + } + }, + [renameTag, tag, hideTagRenameModal], + ); + + const handleShowTagRenameModal = useCallback( + (record: string) => { + setTag(record); + showFileRenameModal(); + }, + [showFileRenameModal], + ); + + return { + renameLoading: loading, + initialName: tag, + onTagRenameOk, + tagRenameVisible, + hideTagRenameModal, + showTagRenameModal: handleShowTagRenameModal, + }; +}; diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/index.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/index.tsx new file mode 100644 index 000000000..e23c4c6a4 --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/index.tsx @@ -0,0 +1,309 @@ +'use client'; + +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react'; +import * as React from 'react'; + +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useDeleteTag, useFetchTagList } from '@/hooks/knowledge-hooks'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useRenameKnowledgeTag } from '../hooks'; +import { RenameDialog } from './rename-dialog'; + +export type ITag = { + tag: string; + frequency: number; +}; + +export function TagTable() { + const { t } = useTranslation(); + const { list } = useFetchTagList(); + const [tagList, setTagList] = useState([]); + + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = useState({}); + + const { deleteTag } = useDeleteTag(); + + useEffect(() => { + setTagList(list.map((x) => ({ tag: x[0], frequency: x[1] }))); + }, [list]); + + const handleDeleteTag = useCallback( + (tags: string[]) => () => { + deleteTag(tags); + }, + [deleteTag], + ); + + const { + showTagRenameModal, + hideTagRenameModal, + tagRenameVisible, + onTagRenameOk, + initialName, + } = useRenameKnowledgeTag(); + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'tag', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value: string = row.getValue('tag'); + return
{value}
; + }, + }, + { + accessorKey: 'frequency', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue('frequency')}
+ ), + }, + { + id: 'actions', + enableHiding: false, + header: t('common.action'), + cell: ({ row }) => { + return ( +
+ + + + + + + +

{t('common.delete')}

+
+
+ + + + + +

{t('common.rename')}

+
+
+
+ ); + }, + }, + ]; + + const table = useReactTable({ + data: tagList, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + const selectedRowLength = table.getFilteredSelectedRowModel().rows.length; + + return ( + +
+
+ + table.getColumn('tag')?.setFilterValue(event.target.value) + } + className="w-1/2" + /> + {selectedRowLength > 0 && ( + x.original.tag), + )} + > + + + )} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {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(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '} + row(s) selected. +
+
+ + +
+
+
+ {tagRenameVisible && ( + + )} +
+ ); +} diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/index.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/index.tsx new file mode 100644 index 000000000..4473eeca1 --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/index.tsx @@ -0,0 +1,38 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { IModalProps } from '@/interfaces/common'; +import { TagRenameId } from '@/pages/add-knowledge/constant'; +import { useTranslation } from 'react-i18next'; +import { RenameForm } from './rename-form'; + +export function RenameDialog({ + hideModal, + initialName, +}: IModalProps & { initialName: string }) { + const { t } = useTranslation(); + + return ( + + + + {t('common.rename')} + + + + + + + + ); +} diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/rename-form.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/rename-form.tsx new file mode 100644 index 000000000..9c8f1cf7e --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-setting/tag-table/rename-dialog/rename-form.tsx @@ -0,0 +1,83 @@ +'use client'; + +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 { Input } from '@/components/ui/input'; +import { useRenameTag } from '@/hooks/knowledge-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { TagRenameId } from '@/pages/add-knowledge/constant'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function RenameForm({ + initialName, + hideModal, +}: IModalProps & { initialName: string }) { + const { t } = useTranslation(); + const FormSchema = z.object({ + name: z + .string() + .min(1, { + message: t('common.namePlaceholder'), + }) + .trim(), + }); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: '', + }, + }); + + const { renameTag } = useRenameTag(); + + async function onSubmit(data: z.infer) { + const ret = await renameTag({ fromTag: initialName, toTag: data.name }); + if (ret) { + hideModal?.(); + } + } + + useEffect(() => { + form.setValue('name', initialName); + }, [form, initialName]); + + return ( +
+ + ( + + {t('common.name')} + + + + + + )} + /> + + + ); +} diff --git a/web/src/pages/add-knowledge/constant.ts b/web/src/pages/add-knowledge/constant.ts index f68a7d0f9..71df266c8 100644 --- a/web/src/pages/add-knowledge/constant.ts +++ b/web/src/pages/add-knowledge/constant.ts @@ -17,3 +17,5 @@ export const datasetRouteMap = { }; export * from '@/constants/knowledge'; + +export const TagRenameId = 'tagRename'; diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index 2804cca58..942bd82be 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -1,6 +1,7 @@ +import { IRenameTag } from '@/interfaces/database/knowledge'; import api from '@/utils/api'; import registerServer from '@/utils/register-server'; -import request from '@/utils/request'; +import request, { post } from '@/utils/request'; const { create_kb, @@ -143,4 +144,15 @@ const methods = { const kbService = registerServer(methods, request); +export const listTag = (knowledgeId: string) => + request.get(api.listTag(knowledgeId)); + +export const removeTag = (knowledgeId: string, tags: string[]) => + post(api.removeTag(knowledgeId), { tags }); + +export const renameTag = ( + knowledgeId: string, + { fromTag, toTag }: IRenameTag, +) => post(api.renameTag(knowledgeId), { fromTag, toTag }); + export default kbService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 182fb3c28..67ad30891 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -37,6 +37,12 @@ export default { rm_kb: `${api_host}/kb/rm`, get_kb_detail: `${api_host}/kb/detail`, + // tags + listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, + removeTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/rm_tags`, + renameTag: (knowledgeId: string) => + `${api_host}/kb/${knowledgeId}/rename_tag`, + // chunk chunk_list: `${api_host}/chunk/list`, create_chunk: `${api_host}/chunk/create`,