From dbc267758e85fe0fb9384dcd81665e05f677a26d Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Mon, 21 Jul 2025 19:11:27 +0800 Subject: [PATCH] Fix: Generate avatar; Add knowledge graph; Modify the style of the MultiSelect component (#8952) ### What problem does this PR solve? Fix: Generate avatar; Add knowledge graph; Modify the style of the multi-select component [#3221](https://github.com/infiniflow/ragflow/issues/3221) ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- web/src/components/ragflow-avatar.tsx | 142 ++++++++--- web/src/components/ui/multi-select.tsx | 25 +- web/src/hooks/use-knowledge-request.ts | 27 +- .../pages/dataset/knowledge-graph/constant.ts | 241 ++++++++++++++++++ .../dataset/knowledge-graph/force-graph.tsx | 141 ++++++++++ .../pages/dataset/knowledge-graph/index.less | 5 + .../pages/dataset/knowledge-graph/index.tsx | 31 +++ .../knowledge-graph/use-delete-graph.ts | 21 ++ web/src/pages/dataset/knowledge-graph/util.ts | 94 +++++++ web/src/pages/dataset/setting/tag-item.tsx | 13 +- web/src/pages/dataset/sidebar/index.tsx | 21 +- web/src/routes.ts | 5 + 12 files changed, 708 insertions(+), 58 deletions(-) create mode 100644 web/src/pages/dataset/knowledge-graph/constant.ts create mode 100644 web/src/pages/dataset/knowledge-graph/force-graph.tsx create mode 100644 web/src/pages/dataset/knowledge-graph/index.less create mode 100644 web/src/pages/dataset/knowledge-graph/index.tsx create mode 100644 web/src/pages/dataset/knowledge-graph/use-delete-graph.ts create mode 100644 web/src/pages/dataset/knowledge-graph/util.ts diff --git a/web/src/components/ragflow-avatar.tsx b/web/src/components/ragflow-avatar.tsx index 728c5e374..e136e5806 100644 --- a/web/src/components/ragflow-avatar.tsx +++ b/web/src/components/ragflow-avatar.tsx @@ -1,44 +1,116 @@ import { cn } from '@/lib/utils'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; -import { random } from 'lodash'; -import { forwardRef } from 'react'; +import { forwardRef, memo, useEffect, useRef, useState } from 'react'; import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; -const Colors = [ - { from: '#4F6DEE', to: '#67BDF9' }, - { from: '#38A04D', to: '#93DCA2' }, - { from: '#EDB395', to: '#C35F2B' }, - { from: '#633897', to: '#CBA1FF' }, -]; +const getStringHash = (str: string): number => { + const normalized = str.trim().toLowerCase(); + let hash = 104729; + const seed = 0x9747b28c; -export const RAGFlowAvatar = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - name?: string; - avatar?: string; - isPerson?: boolean; + for (let i = 0; i < normalized.length; i++) { + hash ^= seed ^ normalized.charCodeAt(i); + hash = (hash << 13) | (hash >>> 19); + hash = (hash * 5 + 0x52dce72d) | 0; } ->(({ name, avatar, isPerson = false, className, ...props }, ref) => { - const index = random(0, 3); - console.log('🚀 ~ index:', index); - const value = Colors[index]; - return ( - - - { + const hash = getStringHash(name); + const hue = hash % 360; + + return { + from: `hsl(${hue}, 70%, 80%)`, + to: `hsl(${hue}, 60%, 30%)`, + }; +}; +export const RAGFlowAvatar = memo( + forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + name?: string; + avatar?: string; + isPerson?: boolean; + } + >(({ name, avatar, isPerson = false, className, ...props }, ref) => { + // Generate initial letter logic + const getInitials = (name?: string) => { + if (!name) return ''; + const parts = name.trim().split(/\s+/); + if (parts.length === 1) { + return parts[0][0].toUpperCase(); + } + return parts[0][0].toUpperCase() + parts[1][0].toUpperCase(); + }; + + const initials = getInitials(name); + const { from, to } = name + ? getColorForName(name) + : { from: 'hsl(0, 0%, 80%)', to: 'hsl(0, 0%, 30%)' }; + + const fallbackRef = useRef(null); + const [fontSize, setFontSize] = useState('0.875rem'); + + // Calculate font size + const calculateFontSize = () => { + if (fallbackRef.current) { + const containerWidth = fallbackRef.current.offsetWidth; + const newSize = containerWidth * 0.6; + setFontSize(`${newSize}px`); + } + }; + + useEffect(() => { + calculateFontSize(); + + if (fallbackRef.current) { + const resizeObserver = new ResizeObserver(() => { + calculateFontSize(); + }); + + resizeObserver.observe(fallbackRef.current); + + return () => { + if (fallbackRef.current) { + resizeObserver.unobserve(fallbackRef.current); + } + resizeObserver.disconnect(); + }; + } + }, []); + + return ( + - {name?.slice(0, 1)} - - - ); -}); + + { + fallbackRef.current = node; + calculateFontSize(); + }} + className={cn( + 'bg-gradient-to-b', + `from-[${from}] to-[${to}]`, + 'flex items-center justify-center', + 'text-white font-bold', + { 'rounded-md': !isPerson }, + )} + style={{ + backgroundImage: `linear-gradient(to bottom, ${from}, ${to})`, + fontSize: fontSize, + }} + > + {initials} + + + ); + }), +); RAGFlowAvatar.displayName = 'RAGFlowAvatar'; diff --git a/web/src/components/ui/multi-select.tsx b/web/src/components/ui/multi-select.tsx index 0c4fb9d68..8f4c8bcac 100644 --- a/web/src/components/ui/multi-select.tsx +++ b/web/src/components/ui/multi-select.tsx @@ -215,23 +215,26 @@ export const MultiSelect = React.forwardRef< return ( - {IconComponent && ( - - )} - {option?.label} - { - event.stopPropagation(); - toggleOption(value); - }} - /> +
+ {IconComponent && ( + + )} +
{option?.label}
+ { + event.stopPropagation(); + toggleOption(value); + }} + /> +
); })} diff --git a/web/src/hooks/use-knowledge-request.ts b/web/src/hooks/use-knowledge-request.ts index d29ad70f9..018ef9a8c 100644 --- a/web/src/hooks/use-knowledge-request.ts +++ b/web/src/hooks/use-knowledge-request.ts @@ -1,12 +1,16 @@ import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; import { IKnowledge, + IKnowledgeGraph, IKnowledgeResult, INextTestingResult, } from '@/interfaces/database/knowledge'; import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge'; import i18n from '@/locales/config'; -import kbService, { listDataset } from '@/services/knowledge-service'; +import kbService, { + getKnowledgeGraph, + listDataset, +} from '@/services/knowledge-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDebounce } from 'ahooks'; import { message } from 'antd'; @@ -26,10 +30,10 @@ export const enum KnowledgeApiAction { FetchKnowledgeDetail = 'fetchKnowledgeDetail', } -export const useKnowledgeBaseId = () => { +export const useKnowledgeBaseId = (): string => { const { id } = useParams(); - return id; + return (id as string) || ''; }; export const useTestRetrieval = () => { @@ -254,3 +258,20 @@ export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { return { data, loading }; }; + +export function useFetchKnowledgeGraph() { + const knowledgeBaseId = useKnowledgeBaseId(); + + const { data, isFetching: loading } = useQuery({ + queryKey: ['fetchKnowledgeGraph', knowledgeBaseId], + initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph, + enabled: !!knowledgeBaseId, + gcTime: 0, + queryFn: async () => { + const { data } = await getKnowledgeGraph(knowledgeBaseId); + return data?.data; + }, + }); + + return { data, loading }; +} diff --git a/web/src/pages/dataset/knowledge-graph/constant.ts b/web/src/pages/dataset/knowledge-graph/constant.ts new file mode 100644 index 000000000..fc7d3561f --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/constant.ts @@ -0,0 +1,241 @@ +const nodes = [ + { + type: '"ORGANIZATION"', + description: + '"厦门象屿是一家公司,其营业收入和市场占有率在2018年至2022年间有所变化。"', + source_id: '0', + id: '"厦门象屿"', + }, + { + type: '"EVENT"', + description: + '"2018年是一个时间点,标志着厦门象屿营业收入和市场占有率的记录开始。"', + source_id: '0', + entity_type: '"EVENT"', + id: '"2018"', + }, + { + type: '"EVENT"', + description: + '"2019年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"', + source_id: '0', + entity_type: '"EVENT"', + id: '"2019"', + }, + { + type: '"EVENT"', + description: + '"2020年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"', + source_id: '0', + entity_type: '"EVENT"', + id: '"2020"', + }, + { + type: '"EVENT"', + description: + '"2021年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"', + source_id: '0', + entity_type: '"EVENT"', + id: '"2021"', + }, + { + type: '"EVENT"', + description: + '"2022年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"', + source_id: '0', + entity_type: '"EVENT"', + id: '"2022"', + }, + { + type: '"ORGANIZATION"', + description: + '"厦门象屿股份有限公司是一家公司,中文简称为厦门象屿,外文名称为Xiamen Xiangyu Co.,Ltd.,外文名称缩写为Xiangyu,法定代表人为邓启东。"', + source_id: '1', + id: '"厦门象屿股份有限公司"', + }, + { + type: '"PERSON"', + description: '"邓启东是厦门象屿股份有限公司的法定代表人。"', + source_id: '1', + entity_type: '"PERSON"', + id: '"邓启东"', + }, + { + type: '"GEO"', + description: '"厦门是一个地理位置,与厦门象屿股份有限公司相关。"', + source_id: '1', + entity_type: '"GEO"', + id: '"厦门"', + }, + { + type: '"PERSON"', + description: + '"廖杰 is the Board Secretary, responsible for handling board-related matters and communications."', + source_id: '2', + id: '"廖杰"', + }, + { + type: '"PERSON"', + description: + '"史经洋 is the Securities Affairs Representative, responsible for handling securities-related matters and communications."', + source_id: '2', + entity_type: '"PERSON"', + id: '"史经洋"', + }, + { + type: '"GEO"', + description: + '"A geographic location in Xiamen, specifically in the Free Trade Zone, where the company\'s office is situated."', + source_id: '2', + entity_type: '"GEO"', + id: '"厦门市湖里区自由贸易试验区厦门片区"', + }, + { + type: '"GEO"', + description: + '"The building where the company\'s office is located, situated at Xiangyu Road, Xiamen."', + source_id: '2', + entity_type: '"GEO"', + id: '"象屿集团大厦"', + }, + { + type: '"EVENT"', + description: + '"Refers to the year 2021, used for comparing financial metrics with the year 2022."', + source_id: '3', + id: '"2021年"', + }, + { + type: '"EVENT"', + description: + '"Refers to the year 2022, used for presenting current financial metrics and comparing them with the year 2021."', + source_id: '3', + entity_type: '"EVENT"', + id: '"2022年"', + }, + { + type: '"EVENT"', + description: + '"Indicates the focus on key financial metrics in the table, such as weighted averages and percentages."', + source_id: '3', + entity_type: '"EVENT"', + id: '"主要财务指标"', + }, +].map(({ type, ...x }) => ({ ...x })); + +const edges = [ + { + weight: 2.0, + description: '"厦门象屿在2018年的营业收入和市场占有率被记录。"', + source_id: '0', + source: '"厦门象屿"', + target: '"2018"', + }, + { + weight: 2.0, + description: '"厦门象屿在2019年的营业收入和市场占有率有所变化。"', + source_id: '0', + source: '"厦门象屿"', + target: '"2019"', + }, + { + weight: 2.0, + description: '"厦门象屿在2020年的营业收入和市场占有率有所变化。"', + source_id: '0', + source: '"厦门象屿"', + target: '"2020"', + }, + { + weight: 2.0, + description: '"厦门象屿在2021年的营业收入和市场占有率有所变化。"', + source_id: '0', + source: '"厦门象屿"', + target: '"2021"', + }, + { + weight: 2.0, + description: '"厦门象屿在2022年的营业收入和市场占有率有所变化。"', + source_id: '0', + source: '"厦门象屿"', + target: '"2022"', + }, + { + weight: 2.0, + description: '"厦门象屿股份有限公司的法定代表人是邓启东。"', + source_id: '1', + source: '"厦门象屿股份有限公司"', + target: '"邓启东"', + }, + { + weight: 2.0, + description: '"厦门象屿股份有限公司位于厦门。"', + source_id: '1', + source: '"厦门象屿股份有限公司"', + target: '"厦门"', + }, + { + weight: 2.0, + description: + '"廖杰\'s office is located in the Xiangyu Group Building, indicating his workplace."', + source_id: '2', + source: '"廖杰"', + target: '"象屿集团大厦"', + }, + { + weight: 2.0, + description: + '"廖杰 works in the Xiamen Free Trade Zone, a specific area within Xiamen."', + source_id: '2', + source: '"廖杰"', + target: '"厦门市湖里区自由贸易试验区厦门片区"', + }, + { + weight: 2.0, + description: + '"史经洋\'s office is also located in the Xiangyu Group Building, indicating his workplace."', + source_id: '2', + source: '"史经洋"', + target: '"象屿集团大厦"', + }, + { + weight: 2.0, + description: + '"史经洋 works in the Xiamen Free Trade Zone, a specific area within Xiamen."', + source_id: '2', + source: '"史经洋"', + target: '"厦门市湖里区自由贸易试验区厦门片区"', + }, + { + weight: 2.0, + description: + '"The years 2021 and 2022 are related as they are used for comparing financial metrics, showing changes and adjustments over time."', + source_id: '3', + source: '"2021年"', + target: '"2022年"', + }, + { + weight: 2.0, + description: + '"The \'主要财务指标\' is related to the year 2021 as it provides the basis for financial comparisons and adjustments."', + source_id: '3', + source: '"2021年"', + target: '"主要财务指标"', + }, + { + weight: 2.0, + description: + '"The \'主要财务指标\' is related to the year 2022 as it presents the current financial metrics and their changes compared to 2021."', + source_id: '3', + source: '"2022年"', + target: '"主要财务指标"', + }, +]; + +export const graphData = { + directed: false, + multigraph: false, + graph: {}, + nodes, + edges, + combos: [], +}; diff --git a/web/src/pages/dataset/knowledge-graph/force-graph.tsx b/web/src/pages/dataset/knowledge-graph/force-graph.tsx new file mode 100644 index 000000000..d2547e959 --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/force-graph.tsx @@ -0,0 +1,141 @@ +import { ElementDatum, Graph, IElementEvent } from '@antv/g6'; +import isEmpty from 'lodash/isEmpty'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { buildNodesAndCombos } from './util'; + +import styles from './index.less'; + +const TooltipColorMap = { + combo: 'red', + node: 'black', + edge: 'blue', +}; + +interface IProps { + data: any; + show: boolean; +} + +const ForceGraph = ({ data, show }: IProps) => { + const containerRef = useRef(null); + const graphRef = useRef(null); + + const nextData = useMemo(() => { + if (!isEmpty(data)) { + const graphData = data; + const mi = buildNodesAndCombos(graphData.nodes); + return { edges: graphData.edges, ...mi }; + } + return { nodes: [], edges: [] }; + }, [data]); + + const render = useCallback(() => { + const graph = new Graph({ + container: containerRef.current!, + autoFit: 'view', + autoResize: true, + behaviors: [ + 'drag-element', + 'drag-canvas', + 'zoom-canvas', + 'collapse-expand', + { + type: 'hover-activate', + degree: 1, // 👈🏻 Activate relations. + }, + ], + plugins: [ + { + type: 'tooltip', + enterable: true, + getContent: (e: IElementEvent, items: ElementDatum) => { + if (Array.isArray(items)) { + if (items.some((x) => x?.isCombo)) { + return `

${items?.[0]?.data?.label}

`; + } + let result = ``; + items.forEach((item) => { + result += `

${item?.id}

`; + if (item?.entity_type) { + result += `
Entity type: ${item?.entity_type}
`; + } + if (item?.weight) { + result += `
Weight: ${item?.weight}
`; + } + if (item?.description) { + result += `

${item?.description}

`; + } + }); + return result + '
'; + } + return undefined; + }, + }, + ], + layout: { + type: 'combo-combined', + preventOverlap: true, + comboPadding: 1, + spacing: 100, + }, + node: { + style: { + size: 150, + labelText: (d) => d.id, + // labelPadding: 30, + labelFontSize: 40, + // labelOffsetX: 20, + labelOffsetY: 20, + labelPlacement: 'center', + labelWordWrap: true, + }, + palette: { + type: 'group', + field: (d) => { + return d?.entity_type as string; + }, + }, + }, + edge: { + style: (model) => { + const weight: number = Number(model?.weight) || 2; + const lineWeight = weight * 4; + return { + stroke: '#99ADD1', + lineWidth: lineWeight > 10 ? 10 : lineWeight, + }; + }, + }, + }); + + if (graphRef.current) { + graphRef.current.destroy(); + } + + graphRef.current = graph; + + graph.setData(nextData); + + graph.render(); + }, [nextData]); + + useEffect(() => { + if (!isEmpty(data)) { + render(); + } + }, [data, render]); + + return ( +
+ ); +}; + +export default ForceGraph; diff --git a/web/src/pages/dataset/knowledge-graph/index.less b/web/src/pages/dataset/knowledge-graph/index.less new file mode 100644 index 000000000..7c5d1f5a8 --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/index.less @@ -0,0 +1,5 @@ +.forceContainer { + :global(.tooltip) { + border-radius: 10px !important; + } +} diff --git a/web/src/pages/dataset/knowledge-graph/index.tsx b/web/src/pages/dataset/knowledge-graph/index.tsx new file mode 100644 index 000000000..42d3749b6 --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/index.tsx @@ -0,0 +1,31 @@ +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { Button } from '@/components/ui/button'; +import { useFetchKnowledgeGraph } from '@/hooks/knowledge-hooks'; +import { Trash2 } from 'lucide-react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ForceGraph from './force-graph'; +import { useDeleteKnowledgeGraph } from './use-delete-graph'; + +const KnowledgeGraph: React.FC = () => { + const { data } = useFetchKnowledgeGraph(); + const { t } = useTranslation(); + const { handleDeleteKnowledgeGraph } = useDeleteKnowledgeGraph(); + + return ( +
+ + + + +
+ ); +}; + +export default KnowledgeGraph; diff --git a/web/src/pages/dataset/knowledge-graph/use-delete-graph.ts b/web/src/pages/dataset/knowledge-graph/use-delete-graph.ts new file mode 100644 index 000000000..49c42986f --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/use-delete-graph.ts @@ -0,0 +1,21 @@ +import { + useKnowledgeBaseId, + useRemoveKnowledgeGraph, +} from '@/hooks/knowledge-hooks'; +import { useCallback } from 'react'; +import { useNavigate } from 'umi'; + +export function useDeleteKnowledgeGraph() { + const { removeKnowledgeGraph, loading } = useRemoveKnowledgeGraph(); + const navigate = useNavigate(); + const knowledgeBaseId = useKnowledgeBaseId(); + + const handleDeleteKnowledgeGraph = useCallback(async () => { + const ret = await removeKnowledgeGraph(); + if (ret === 0) { + navigate(`/knowledge/dataset?id=${knowledgeBaseId}`); + } + }, [knowledgeBaseId, navigate, removeKnowledgeGraph]); + + return { handleDeleteKnowledgeGraph, loading }; +} diff --git a/web/src/pages/dataset/knowledge-graph/util.ts b/web/src/pages/dataset/knowledge-graph/util.ts new file mode 100644 index 000000000..e0be797f2 --- /dev/null +++ b/web/src/pages/dataset/knowledge-graph/util.ts @@ -0,0 +1,94 @@ +import { isEmpty } from 'lodash'; +import { v4 as uuid } from 'uuid'; + +class KeyGenerator { + idx = 0; + chars: string[] = []; + constructor() { + const chars = Array(26) + .fill(1) + .map((x, idx) => String.fromCharCode(97 + idx)); // 26 char + this.chars = chars; + } + generateKey() { + const key = this.chars[this.idx]; + this.idx++; + return key; + } +} + +// Classify nodes based on edge relationships +export class Converter { + keyGenerator; + dict: Record = {}; // key is node id, value is combo + constructor() { + this.keyGenerator = new KeyGenerator(); + } + buildDict(edges: { source: string; target: string }[]) { + edges.forEach((x) => { + if (this.dict[x.source] && !this.dict[x.target]) { + this.dict[x.target] = this.dict[x.source]; + } else if (!this.dict[x.source] && this.dict[x.target]) { + this.dict[x.source] = this.dict[x.target]; + } else if (!this.dict[x.source] && !this.dict[x.target]) { + this.dict[x.source] = this.dict[x.target] = + this.keyGenerator.generateKey(); + } + }); + return this.dict; + } + buildNodesAndCombos(nodes: any[], edges: any[]) { + this.buildDict(edges); + const nextNodes = nodes.map((x) => ({ ...x, combo: this.dict[x.id] })); + + const combos = Object.values(this.dict).reduce((pre, cur) => { + if (pre.every((x) => x.id !== cur)) { + pre.push({ + id: cur, + data: { + label: `Combo ${cur}`, + }, + }); + } + return pre; + }, []); + + return { nodes: nextNodes, combos }; + } +} + +export const isDataExist = (data: any) => { + return ( + data?.data && typeof data?.data !== 'boolean' && !isEmpty(data?.data?.graph) + ); +}; + +const findCombo = (communities: string[]) => { + const combo = Array.isArray(communities) ? communities[0] : undefined; + return combo; +}; + +export const buildNodesAndCombos = (nodes: any[]) => { + const combos: any[] = []; + nodes.forEach((x) => { + const combo = findCombo(x?.communities); + if (combo && combos.every((y) => y.data.label !== combo)) { + combos.push({ + isCombo: true, + id: uuid(), + data: { + label: combo, + }, + }); + } + }); + + const nextNodes = nodes.map((x) => { + return { + ...x, + combo: combos.find((y) => y.data.label === findCombo(x?.communities))?.id, + }; + }); + + return { nodes: nextNodes, combos }; +}; diff --git a/web/src/pages/dataset/setting/tag-item.tsx b/web/src/pages/dataset/setting/tag-item.tsx index 9463253de..c5ccecca4 100644 --- a/web/src/pages/dataset/setting/tag-item.tsx +++ b/web/src/pages/dataset/setting/tag-item.tsx @@ -1,3 +1,4 @@ +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { SliderInputFormField } from '@/components/slider-input-form-field'; import { FormControl, @@ -8,8 +9,7 @@ import { } 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 { Flex, Form, InputNumber, Select, Slider, Space } from 'antd'; import DOMPurify from 'dompurify'; import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -27,8 +27,11 @@ export const TagSetItem = () => { value: x.id, icon: () => ( - } src={x.avatar} /> - {x.name} + ), })); @@ -61,7 +64,7 @@ export const TagSetItem = () => { onValueChange={field.onChange} placeholder={t('chat.knowledgeBasesMessage')} variant="inverted" - maxCount={0} + maxCount={10} {...field} /> diff --git a/web/src/pages/dataset/sidebar/index.tsx b/web/src/pages/dataset/sidebar/index.tsx index 59accbec9..f3b65c71b 100644 --- a/web/src/pages/dataset/sidebar/index.tsx +++ b/web/src/pages/dataset/sidebar/index.tsx @@ -1,11 +1,15 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { Button } from '@/components/ui/button'; import { useSecondPathName } from '@/hooks/route-hook'; -import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; +import { + useFetchKnowledgeBaseConfiguration, + useFetchKnowledgeGraph, +} from '@/hooks/use-knowledge-request'; import { cn, formatBytes } from '@/lib/utils'; import { Routes } from '@/routes'; import { formatPureDate } from '@/utils/date'; -import { Banknote, Database, FileSearch2 } from 'lucide-react'; +import { isEmpty } from 'lodash'; +import { Banknote, Database, FileSearch2, GitGraph } from 'lucide-react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useHandleMenuClick } from './hooks'; @@ -19,10 +23,11 @@ export function SideBar({ refreshCount }: PropType) { const { handleMenuClick } = useHandleMenuClick(); // refreshCount: be for avatar img sync update on top left const { data } = useFetchKnowledgeBaseConfiguration(refreshCount); + const { data: routerData } = useFetchKnowledgeGraph(); const { t } = useTranslation(); const items = useMemo(() => { - return [ + const list = [ { icon: Database, label: t(`knowledgeDetails.dataset`), @@ -39,7 +44,15 @@ export function SideBar({ refreshCount }: PropType) { key: Routes.DatasetSetting, }, ]; - }, [t]); + if (!isEmpty(routerData?.graph)) { + list.push({ + icon: GitGraph, + label: t(`knowledgeDetails.knowledgeGraph`), + key: Routes.KnowledgeGraph, + }); + } + return list; + }, [t, routerData]); return (