diff --git a/web/src/components/page-rank-form-field.tsx b/web/src/components/page-rank-form-field.tsx new file mode 100644 index 000000000..4ce23d6fc --- /dev/null +++ b/web/src/components/page-rank-form-field.tsx @@ -0,0 +1,19 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { SliderInputFormField } from './slider-input-form-field'; + +export function PageRankFormField() { + const { t } = useTranslate('knowledgeConfiguration'); + + return ( + + ); +} + +export default PageRankFormField; diff --git a/web/src/components/parse-configuration/graph-rag-form-fields.tsx b/web/src/components/parse-configuration/graph-rag-form-fields.tsx index c92ca5697..30f8f390b 100644 --- a/web/src/components/parse-configuration/graph-rag-form-fields.tsx +++ b/web/src/components/parse-configuration/graph-rag-form-fields.tsx @@ -1,12 +1,11 @@ import { DocumentParserType } from '@/constants/knowledge'; import { useTranslate } from '@/hooks/common-hooks'; import { cn } from '@/lib/utils'; -import { Switch as AntSwitch, Form, Select } from 'antd'; import { upperFirst } from 'lodash'; import { useCallback, useMemo } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { DatasetConfigurationContainer } from '../dataset-configuration-container'; -import EntityTypesItem from '../entity-types-item'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { EntityTypesFormField } from '../entity-types-form-field'; +import { FormContainer } from '../form-container'; import { FormControl, FormField, @@ -14,6 +13,7 @@ import { FormLabel, FormMessage, } from '../ui/form'; +import { RAGFlowSelect } from '../ui/select'; import { Switch } from '../ui/switch'; const excludedTagParseMethods = [ @@ -48,22 +48,6 @@ type GraphRagItemsProps = { marginBottom?: boolean; }; -export function UseGraphRagItem() { - const { t } = useTranslate('knowledgeConfiguration'); - - return ( - - - - ); -} - export function UseGraphRagFormField() { const form = useFormContext(); const { t } = useTranslate('knowledgeConfiguration'); @@ -93,6 +77,12 @@ export function UseGraphRagFormField() { // The three types "table", "resume" and "one" do not display this configuration. const GraphRagItems = ({ marginBottom = false }: GraphRagItemsProps) => { const { t } = useTranslate('knowledgeConfiguration'); + const form = useFormContext(); + + const useRaptor = useWatch({ + control: form.control, + name: 'parser_config.graphrag.use_graphrag', + }); const methodOptions = useMemo(() => { return [MethodValue.Light, MethodValue.General].map((x) => ({ @@ -103,39 +93,23 @@ const GraphRagItems = ({ marginBottom = false }: GraphRagItemsProps) => { const renderWideTooltip = useCallback( (title: React.ReactNode | string) => { - return { - title: typeof title === 'string' ? t(title) : title, - overlayInnerStyle: { width: '32vw' }, - }; + return typeof title === 'string' ? t(title) : title; }, [t], ); return ( - - - - prevValues.parser_config.graphrag.use_graphrag !== - curValues.parser_config.graphrag.use_graphrag - } - > - {({ getFieldValue }) => { - const useRaptor = getFieldValue([ - 'parser_config', - 'graphrag', - 'use_graphrag', - ]); - - return ( - useRaptor && ( - <> - - + + {useRaptor && ( + <> + + ( + + { }} >, )} - initialValue={MethodValue.Light} > - + + + + )} + /> + ( + + {t('knowledgeConfiguration.description')} + + + + + + )} + /> + ( + + {t('knowledgeConfiguration.photo')} + + + + + + )} + /> + + ( + + + {t('knowledgeConfiguration.permissions')} + + + + + + + + + {t('knowledgeConfiguration.me')} + + + + + + + + {t('knowledgeConfiguration.team')} + + + + + + + )} + /> + + ); +} diff --git a/web/src/pages/dataset/setting/hooks.ts b/web/src/pages/dataset/setting/hooks.ts new file mode 100644 index 000000000..e20b63516 --- /dev/null +++ b/web/src/pages/dataset/setting/hooks.ts @@ -0,0 +1,123 @@ +import { LlmModelType } from '@/constants/knowledge'; +import { useSetModalState } from '@/hooks/common-hooks'; + +import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks'; +import { useNavigateToDataset } from '@/hooks/route-hook'; +import { + useFetchKnowledgeBaseConfiguration, + useUpdateKnowledge, +} from '@/hooks/use-knowledge-request'; +import { useSelectParserList } from '@/hooks/user-setting-hooks'; +import { + getBase64FromUploadFileList, + getUploadFileListFromBase64, +} from '@/utils/file-util'; +import { useIsFetching } from '@tanstack/react-query'; +import { Form, UploadFile } from 'antd'; +import { FormInstance } from 'antd/lib'; +import pick from 'lodash/pick'; +import { useCallback, useEffect, useState } from 'react'; +import { UseFormReturn } from 'react-hook-form'; + +export const useSubmitKnowledgeConfiguration = (form: FormInstance) => { + const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge(); + const navigateToDataset = useNavigateToDataset(); + + const submitKnowledgeConfiguration = useCallback(async () => { + const values = await form.validateFields(); + const avatar = await getBase64FromUploadFileList(values.avatar); + saveKnowledgeConfiguration({ + ...values, + avatar, + }); + navigateToDataset(); + }, [saveKnowledgeConfiguration, form, navigateToDataset]); + + return { + submitKnowledgeConfiguration, + submitLoading: loading, + navigateToDataset, + }; +}; + +// The value that does not need to be displayed in the analysis method Select +const HiddenFields = ['email', 'picture', 'audio']; + +export function useSelectChunkMethodList() { + const parserList = useSelectParserList(); + + return parserList.filter((x) => !HiddenFields.some((y) => y === x.value)); +} + +export function useSelectEmbeddingModelOptions() { + const allOptions = useSelectLlmOptionsByModelType(); + return allOptions[LlmModelType.Embedding]; +} + +export function useHasParsedDocument() { + const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration(); + return knowledgeDetails.chunk_num > 0; +} + +export const useFetchKnowledgeConfigurationOnMount = (form: UseFormReturn) => { + const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration(); + + useEffect(() => { + const fileList: UploadFile[] = getUploadFileListFromBase64( + knowledgeDetails.avatar, + ); + form.reset({ + ...pick(knowledgeDetails, [ + 'description', + 'name', + 'permission', + 'embd_id', + 'parser_id', + 'language', + 'parser_config', + 'pagerank', + ]), + avatar: fileList, + }); + }, [form, knowledgeDetails]); + + return knowledgeDetails; +}; + +export const useSelectKnowledgeDetailsLoading = () => + useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0; + +export const useHandleChunkMethodChange = () => { + const [form] = Form.useForm(); + const chunkMethod = Form.useWatch('parser_id', form); + + useEffect(() => { + console.log('🚀 ~ useHandleChunkMethodChange ~ chunkMethod:', chunkMethod); + }, [chunkMethod]); + + return { form, chunkMethod }; +}; + +export const useRenameKnowledgeTag = () => { + const [tag, setTag] = useState(''); + const { + visible: tagRenameVisible, + hideModal: hideTagRenameModal, + showModal: showFileRenameModal, + } = useSetModalState(); + + const handleShowTagRenameModal = useCallback( + (record: string) => { + setTag(record); + showFileRenameModal(); + }, + [showFileRenameModal], + ); + + return { + initialName: tag, + tagRenameVisible, + hideTagRenameModal, + showTagRenameModal: handleShowTagRenameModal, + }; +}; diff --git a/web/src/pages/dataset/setting/index.tsx b/web/src/pages/dataset/setting/index.tsx index 93278fc6d..bbbb73ee6 100644 --- a/web/src/pages/dataset/setting/index.tsx +++ b/web/src/pages/dataset/setting/index.tsx @@ -1,25 +1,137 @@ -import { Card, CardContent } from '@/components/ui/card'; -import AdvancedSettingForm from './advanced-setting-form'; -import BasicSettingForm from './basic-setting-form'; +import { Form } from '@/components/ui/form'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { DocumentParserType } from '@/constants/knowledge'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import CategoryPanel from './category-panel'; +import { ChunkMethodForm } from './chunk-method-form'; +import { GeneralForm } from './general-form'; + +const enum DocumentType { + DeepDOC = 'DeepDOC', + PlainText = 'Plain Text', +} + +const initialEntityTypes = [ + 'organization', + 'person', + 'geo', + 'event', + 'category', +]; + +const enum MethodValue { + General = 'general', + Light = 'light', +} export default function DatasetSettings() { - return ( -
-
Basic settings
- - -
- -
-
-
+ const formSchema = z.object({ + name: z.string().min(1, { + message: 'Username must be at least 2 characters.', + }), + description: z.string().min(2, { + message: 'Username must be at least 2 characters.', + }), + avatar: z.instanceof(File), + permission: z.string(), + parser_id: z.string(), + parser_config: z.object({ + layout_recognize: z.string(), + chunk_token_num: z.number(), + delimiter: z.string(), + auto_keywords: z.number(), + auto_questions: z.number(), + html4excel: z.boolean(), + tag_kb_ids: z.array(z.string()), + topn_tags: z.number(), + raptor: z.object({ + use_raptor: z.boolean(), + prompt: z.string(), + max_token: z.number(), + threshold: z.number(), + max_cluster: z.number(), + random_seed: z.number(), + }), + graphrag: z.object({ + use_graphrag: z.boolean(), + entity_types: z.array(z.string()), + method: z.string(), + resolution: z.boolean(), + community: z.boolean(), + }), + }), + pagerank: z.number(), + // icon: z.array(z.instanceof(File)), + }); -
Advanced settings
- - - - - + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + parser_id: DocumentParserType.Naive, + permission: 'me', + parser_config: { + layout_recognize: DocumentType.DeepDOC, + chunk_token_num: 512, + delimiter: `\n`, + auto_keywords: 0, + auto_questions: 0, + html4excel: false, + topn_tags: 3, + raptor: { + use_raptor: false, + max_token: 256, + threshold: 0.1, + max_cluster: 64, + random_seed: 0, + }, + graphrag: { + use_graphrag: false, + entity_types: initialEntityTypes, + method: MethodValue.Light, + }, + }, + pagerank: 0, + }, + }); + + async function onSubmit(data: z.infer) { + console.log('🚀 ~ DatasetSettings ~ data:', data); + } + + return ( +
+
+
Configuration
+

+ Update your knowledge base configuration here, particularly the chunk + method. +

+
+
+
+ + + + Account + Password + + + + + + + + +
+ + +
); } diff --git a/web/src/pages/dataset/setting/tag-item.tsx b/web/src/pages/dataset/setting/tag-item.tsx new file mode 100644 index 000000000..01c5c6f3f --- /dev/null +++ b/web/src/pages/dataset/setting/tag-item.tsx @@ -0,0 +1,144 @@ +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { MultiSelect } from '@/components/ui/multi-select'; +import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Flex, Form, InputNumber, Select, Slider, Space } from 'antd'; +import DOMPurify from 'dompurify'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +export const TagSetItem = () => { + const { t } = useTranslation(); + const form = useFormContext(); + + const { list: knowledgeList } = useFetchKnowledgeList(true); + + const knowledgeOptions = knowledgeList + .filter((x) => x.parser_id === 'tag') + .map((x) => ({ + label: x.name, + value: x.id, + icon: () => ( + + } src={x.avatar} /> + {x.name} + + ), + })); + + return ( + ( + + + } + > + {t('knowledgeConfiguration.tagSet')} + + + + + + + )} + /> + ); + + return ( + + } + rules={[ + { + message: t('chat.knowledgeBasesMessage'), + type: 'array', + }, + ]} + > + + + ); +}; + +export const TopNTagsItem = () => { + const { t } = useTranslation(); + + return ( + + ); + + return ( + + + + + + + + + + + + + ); +}; + +export function TagItems() { + const form = useFormContext(); + const ids: string[] = useWatch({ + control: form.control, + name: 'parser_config.tag_kb_ids', + }); + + return ( + <> + + {Array.isArray(ids) && ids.length > 0 && } + + ); +} diff --git a/web/src/pages/dataset/setting/tag-table/index.tsx b/web/src/pages/dataset/setting/tag-table/index.tsx new file mode 100644 index 000000000..0ab25a86e --- /dev/null +++ b/web/src/pages/dataset/setting/tag-table/index.tsx @@ -0,0 +1,307 @@ +'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, + 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/dataset/setting/tag-table/rename-dialog/index.tsx b/web/src/pages/dataset/setting/tag-table/rename-dialog/index.tsx new file mode 100644 index 000000000..b95907f92 --- /dev/null +++ b/web/src/pages/dataset/setting/tag-table/rename-dialog/index.tsx @@ -0,0 +1,40 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { LoadingButton } from '@/components/ui/loading-button'; +import { useTagIsRenaming } from '@/hooks/knowledge-hooks'; +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(); + const loading = useTagIsRenaming(); + + return ( + + + + {t('common.rename')} + + + + + {t('common.save')} + + + + + ); +} diff --git a/web/src/pages/dataset/setting/tag-table/rename-dialog/rename-form.tsx b/web/src/pages/dataset/setting/tag-table/rename-dialog/rename-form.tsx new file mode 100644 index 000000000..9c8f1cf7e --- /dev/null +++ b/web/src/pages/dataset/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/dataset/setting/tag-tabs.tsx b/web/src/pages/dataset/setting/tag-tabs.tsx new file mode 100644 index 000000000..abcd3f673 --- /dev/null +++ b/web/src/pages/dataset/setting/tag-tabs.tsx @@ -0,0 +1,40 @@ +import { Segmented } from 'antd'; +import { SegmentedLabeledOption } from 'antd/es/segmented'; +import { upperFirst } from 'lodash'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TagTable } from './tag-table'; +import { TagWordCloud } from './tag-word-cloud'; + +enum TagType { + Cloud = 'cloud', + Table = 'table', +} + +const TagContentMap = { + [TagType.Cloud]: , + [TagType.Table]: , +}; + +export function TagTabs() { + const [value, setValue] = useState(TagType.Cloud); + const { t } = useTranslation(); + + const options: SegmentedLabeledOption[] = [TagType.Cloud, TagType.Table].map( + (x) => ({ + label: t(`knowledgeConfiguration.tag${upperFirst(x)}`), + value: x, + }), + ); + + return ( +
+ setValue(val as TagType)} + /> + {TagContentMap[value]} +
+ ); +} diff --git a/web/src/pages/dataset/setting/tag-word-cloud.tsx b/web/src/pages/dataset/setting/tag-word-cloud.tsx new file mode 100644 index 000000000..b71ed69af --- /dev/null +++ b/web/src/pages/dataset/setting/tag-word-cloud.tsx @@ -0,0 +1,62 @@ +import { useFetchTagList } from '@/hooks/knowledge-hooks'; +import { Chart } from '@antv/g2'; +import { sumBy } from 'lodash'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +export function TagWordCloud() { + const domRef = useRef(null); + let chartRef = useRef(); + const { list } = useFetchTagList(); + + const { list: tagList } = useMemo(() => { + const nextList = list.sort((a, b) => b[1] - a[1]).slice(0, 256); + + return { + list: nextList.map((x) => ({ text: x[0], value: x[1], name: x[0] })), + sumValue: sumBy(nextList, (x: [string, number]) => x[1]), + length: nextList.length, + }; + }, [list]); + + const renderWordCloud = useCallback(() => { + if (domRef.current) { + chartRef.current = new Chart({ container: domRef.current }); + + chartRef.current.options({ + type: 'wordCloud', + autoFit: true, + layout: { + fontSize: [10, 50], + // fontSize: (d: any) => { + // if (d.value) { + // return (d.value / sumValue) * 100 * (length / 10); + // } + // return 0; + // }, + }, + data: { + type: 'inline', + value: tagList, + }, + encode: { color: 'text' }, + legend: false, + tooltip: { + title: 'name', // title + items: ['value'], // data item + }, + }); + + chartRef.current.render(); + } + }, [tagList]); + + useEffect(() => { + renderWordCloud(); + + return () => { + chartRef.current?.destroy(); + }; + }, [renderWordCloud]); + + return
; +}