mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
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)
This commit is contained in:
@ -1,44 +1,116 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||||
import { random } from 'lodash';
|
import { forwardRef, memo, useEffect, useRef, useState } from 'react';
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
|
|
||||||
const Colors = [
|
const getStringHash = (str: string): number => {
|
||||||
{ from: '#4F6DEE', to: '#67BDF9' },
|
const normalized = str.trim().toLowerCase();
|
||||||
{ from: '#38A04D', to: '#93DCA2' },
|
let hash = 104729;
|
||||||
{ from: '#EDB395', to: '#C35F2B' },
|
const seed = 0x9747b28c;
|
||||||
{ from: '#633897', to: '#CBA1FF' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const RAGFlowAvatar = forwardRef<
|
for (let i = 0; i < normalized.length; i++) {
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
hash ^= seed ^ normalized.charCodeAt(i);
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
|
hash = (hash << 13) | (hash >>> 19);
|
||||||
name?: string;
|
hash = (hash * 5 + 0x52dce72d) | 0;
|
||||||
avatar?: string;
|
|
||||||
isPerson?: boolean;
|
|
||||||
}
|
}
|
||||||
>(({ name, avatar, isPerson = false, className, ...props }, ref) => {
|
|
||||||
const index = random(0, 3);
|
return Math.abs(hash);
|
||||||
console.log('🚀 ~ index:', index);
|
};
|
||||||
const value = Colors[index];
|
|
||||||
return (
|
// Generate a hash function with a fixed color
|
||||||
<Avatar
|
const getColorForName = (name: string): { from: string; to: string } => {
|
||||||
ref={ref}
|
const hash = getStringHash(name);
|
||||||
{...props}
|
const hue = hash % 360;
|
||||||
className={cn(className, { 'rounded-md': !isPerson })}
|
|
||||||
>
|
return {
|
||||||
<AvatarImage src={avatar} />
|
from: `hsl(${hue}, 70%, 80%)`,
|
||||||
<AvatarFallback
|
to: `hsl(${hue}, 60%, 30%)`,
|
||||||
className={cn(
|
};
|
||||||
`bg-gradient-to-b from-[${value.from}] to-[${value.to}]`,
|
};
|
||||||
{ 'rounded-md': !isPerson },
|
export const RAGFlowAvatar = memo(
|
||||||
)}
|
forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
|
||||||
|
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<HTMLElement>(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 (
|
||||||
|
<Avatar
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={cn(className, { 'rounded-md': !isPerson })}
|
||||||
>
|
>
|
||||||
{name?.slice(0, 1)}
|
<AvatarImage src={avatar} />
|
||||||
</AvatarFallback>
|
<AvatarFallback
|
||||||
</Avatar>
|
ref={(node) => {
|
||||||
);
|
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}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
RAGFlowAvatar.displayName = 'RAGFlowAvatar';
|
RAGFlowAvatar.displayName = 'RAGFlowAvatar';
|
||||||
|
|||||||
@ -215,23 +215,26 @@ export const MultiSelect = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={value}
|
key={value}
|
||||||
|
variant="secondary"
|
||||||
className={cn(
|
className={cn(
|
||||||
isAnimating ? 'animate-bounce' : '',
|
isAnimating ? 'animate-bounce' : '',
|
||||||
multiSelectVariants({ variant }),
|
multiSelectVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
style={{ animationDuration: `${animation}s` }}
|
style={{ animationDuration: `${animation}s` }}
|
||||||
>
|
>
|
||||||
{IconComponent && (
|
<div className="flex items-center gap-1">
|
||||||
<IconComponent className="h-4 w-4 mr-2" />
|
{IconComponent && (
|
||||||
)}
|
<IconComponent className="h-4 w-4" />
|
||||||
{option?.label}
|
)}
|
||||||
<XCircle
|
<div>{option?.label}</div>
|
||||||
className="ml-2 h-4 w-4 cursor-pointer"
|
<XCircle
|
||||||
onClick={(event) => {
|
className="h-4 w-4 cursor-pointer"
|
||||||
event.stopPropagation();
|
onClick={(event) => {
|
||||||
toggleOption(value);
|
event.stopPropagation();
|
||||||
}}
|
toggleOption(value);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
|
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
|
||||||
import {
|
import {
|
||||||
IKnowledge,
|
IKnowledge,
|
||||||
|
IKnowledgeGraph,
|
||||||
IKnowledgeResult,
|
IKnowledgeResult,
|
||||||
INextTestingResult,
|
INextTestingResult,
|
||||||
} from '@/interfaces/database/knowledge';
|
} from '@/interfaces/database/knowledge';
|
||||||
import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
|
import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
|
||||||
import i18n from '@/locales/config';
|
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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useDebounce } from 'ahooks';
|
import { useDebounce } from 'ahooks';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
@ -26,10 +30,10 @@ export const enum KnowledgeApiAction {
|
|||||||
FetchKnowledgeDetail = 'fetchKnowledgeDetail',
|
FetchKnowledgeDetail = 'fetchKnowledgeDetail',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useKnowledgeBaseId = () => {
|
export const useKnowledgeBaseId = (): string => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
return id;
|
return (id as string) || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTestRetrieval = () => {
|
export const useTestRetrieval = () => {
|
||||||
@ -254,3 +258,20 @@ export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => {
|
|||||||
|
|
||||||
return { data, loading };
|
return { data, loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useFetchKnowledgeGraph() {
|
||||||
|
const knowledgeBaseId = useKnowledgeBaseId();
|
||||||
|
|
||||||
|
const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|||||||
241
web/src/pages/dataset/knowledge-graph/constant.ts
Normal file
241
web/src/pages/dataset/knowledge-graph/constant.ts
Normal file
@ -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: [],
|
||||||
|
};
|
||||||
141
web/src/pages/dataset/knowledge-graph/force-graph.tsx
Normal file
141
web/src/pages/dataset/knowledge-graph/force-graph.tsx
Normal file
@ -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<HTMLDivElement>(null);
|
||||||
|
const graphRef = useRef<Graph | null>(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 `<p style="font-weight:600;color:red">${items?.[0]?.data?.label}</p>`;
|
||||||
|
}
|
||||||
|
let result = ``;
|
||||||
|
items.forEach((item) => {
|
||||||
|
result += `<section style="color:${TooltipColorMap[e['targetType'] as keyof typeof TooltipColorMap]};"><h3>${item?.id}</h3>`;
|
||||||
|
if (item?.entity_type) {
|
||||||
|
result += `<div style="padding-bottom: 6px;"><b>Entity type: </b>${item?.entity_type}</div>`;
|
||||||
|
}
|
||||||
|
if (item?.weight) {
|
||||||
|
result += `<div><b>Weight: </b>${item?.weight}</div>`;
|
||||||
|
}
|
||||||
|
if (item?.description) {
|
||||||
|
result += `<p>${item?.description}</p>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result + '</section>';
|
||||||
|
}
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={styles.forceContainer}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: show ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForceGraph;
|
||||||
5
web/src/pages/dataset/knowledge-graph/index.less
Normal file
5
web/src/pages/dataset/knowledge-graph/index.less
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.forceContainer {
|
||||||
|
:global(.tooltip) {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
web/src/pages/dataset/knowledge-graph/index.tsx
Normal file
31
web/src/pages/dataset/knowledge-graph/index.tsx
Normal file
@ -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 (
|
||||||
|
<section className={'w-full h-[90dvh] relative p-6'}>
|
||||||
|
<ConfirmDeleteDialog onOk={handleDeleteKnowledgeGraph}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size={'sm'}
|
||||||
|
className="absolute right-0 top-0 z-50"
|
||||||
|
>
|
||||||
|
<Trash2 /> {t('common.delete')}
|
||||||
|
</Button>
|
||||||
|
</ConfirmDeleteDialog>
|
||||||
|
<ForceGraph data={data?.graph} show></ForceGraph>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KnowledgeGraph;
|
||||||
21
web/src/pages/dataset/knowledge-graph/use-delete-graph.ts
Normal file
21
web/src/pages/dataset/knowledge-graph/use-delete-graph.ts
Normal file
@ -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 };
|
||||||
|
}
|
||||||
94
web/src/pages/dataset/knowledge-graph/util.ts
Normal file
94
web/src/pages/dataset/knowledge-graph/util.ts
Normal file
@ -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<string, string> = {}; // 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<any[]>((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 };
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -8,8 +9,7 @@ import {
|
|||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { MultiSelect } from '@/components/ui/multi-select';
|
import { MultiSelect } from '@/components/ui/multi-select';
|
||||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||||
import { UserOutlined } from '@ant-design/icons';
|
import { Flex, Form, InputNumber, Select, Slider, Space } from 'antd';
|
||||||
import { Avatar, Flex, Form, InputNumber, Select, Slider, Space } from 'antd';
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -27,8 +27,11 @@ export const TagSetItem = () => {
|
|||||||
value: x.id,
|
value: x.id,
|
||||||
icon: () => (
|
icon: () => (
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar size={20} icon={<UserOutlined />} src={x.avatar} />
|
<RAGFlowAvatar
|
||||||
{x.name}
|
name={x.name}
|
||||||
|
avatar={x.avatar}
|
||||||
|
className="size-4"
|
||||||
|
></RAGFlowAvatar>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@ -61,7 +64,7 @@ export const TagSetItem = () => {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
placeholder={t('chat.knowledgeBasesMessage')}
|
placeholder={t('chat.knowledgeBasesMessage')}
|
||||||
variant="inverted"
|
variant="inverted"
|
||||||
maxCount={0}
|
maxCount={10}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
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 {
|
||||||
|
useFetchKnowledgeBaseConfiguration,
|
||||||
|
useFetchKnowledgeGraph,
|
||||||
|
} from '@/hooks/use-knowledge-request';
|
||||||
import { cn, formatBytes } from '@/lib/utils';
|
import { cn, formatBytes } from '@/lib/utils';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import { formatPureDate } from '@/utils/date';
|
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 { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHandleMenuClick } from './hooks';
|
import { useHandleMenuClick } from './hooks';
|
||||||
@ -19,10 +23,11 @@ export function SideBar({ refreshCount }: PropType) {
|
|||||||
const { handleMenuClick } = useHandleMenuClick();
|
const { handleMenuClick } = useHandleMenuClick();
|
||||||
// refreshCount: be for avatar img sync update on top left
|
// refreshCount: be for avatar img sync update on top left
|
||||||
const { data } = useFetchKnowledgeBaseConfiguration(refreshCount);
|
const { data } = useFetchKnowledgeBaseConfiguration(refreshCount);
|
||||||
|
const { data: routerData } = useFetchKnowledgeGraph();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return [
|
const list = [
|
||||||
{
|
{
|
||||||
icon: Database,
|
icon: Database,
|
||||||
label: t(`knowledgeDetails.dataset`),
|
label: t(`knowledgeDetails.dataset`),
|
||||||
@ -39,7 +44,15 @@ export function SideBar({ refreshCount }: PropType) {
|
|||||||
key: Routes.DatasetSetting,
|
key: Routes.DatasetSetting,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [t]);
|
if (!isEmpty(routerData?.graph)) {
|
||||||
|
list.push({
|
||||||
|
icon: GitGraph,
|
||||||
|
label: t(`knowledgeDetails.knowledgeGraph`),
|
||||||
|
key: Routes.KnowledgeGraph,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, [t, routerData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="relative p-5 space-y-8">
|
<aside className="relative p-5 space-y-8">
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export enum Routes {
|
|||||||
ParsedResult = `${Chunk}${Parsed}`,
|
ParsedResult = `${Chunk}${Parsed}`,
|
||||||
Result = '/result',
|
Result = '/result',
|
||||||
ResultView = `${Chunk}${Result}`,
|
ResultView = `${Chunk}${Result}`,
|
||||||
|
KnowledgeGraph = '/knowledge-graph',
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@ -273,6 +274,10 @@ const routes = [
|
|||||||
path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`,
|
path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`,
|
||||||
component: `@/pages${Routes.DatasetBase}${Routes.DatasetTesting}`,
|
component: `@/pages${Routes.DatasetBase}${Routes.DatasetTesting}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`,
|
||||||
|
component: `@/pages${Routes.DatasetBase}${Routes.KnowledgeGraph}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user