mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 21:37:33 +08:00
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
import { SkeletonCard } from '@/components/skeleton-card';
|
|
import { cn } from '@/lib/utils';
|
|
import { Graph, IElementEvent, NodeEvent, treeToGraphData } from '@antv/g6';
|
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
|
|
import { adaptMindMapToIndentedTree } from '../adapters';
|
|
import { type MindMapG6GraphProps, type MindMapNodeValue } from './types';
|
|
|
|
interface MindMapNodeData {
|
|
name?: string;
|
|
source_chunk_ids?: string[];
|
|
}
|
|
|
|
function MindMapG6Graph({
|
|
template,
|
|
show = true,
|
|
onNodeClick,
|
|
}: MindMapG6GraphProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const graphRef = useRef<Graph | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [graphData, setGraphData] = useState<ReturnType<
|
|
typeof treeToGraphData
|
|
> | null>(null);
|
|
|
|
// Defer heavy data transformation to avoid blocking the render phase.
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
setGraphData(null);
|
|
|
|
const rafId = requestAnimationFrame(() => {
|
|
setGraphData(treeToGraphData(adaptMindMapToIndentedTree(template)));
|
|
});
|
|
|
|
return () => {
|
|
cancelAnimationFrame(rafId);
|
|
};
|
|
}, [template]);
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
if (!container || !graphData) return;
|
|
|
|
const styles = window.getComputedStyle(container);
|
|
const textPrimary = styles.getPropertyValue('--text-primary').trim();
|
|
const textSecondary = styles.getPropertyValue('--text-secondary').trim();
|
|
const accentPrimary = styles.getPropertyValue('--accent-primary').trim();
|
|
|
|
let graph: Graph | null = null;
|
|
let observer: ResizeObserver | null = null;
|
|
|
|
const createGraph = () => {
|
|
if (graph) return;
|
|
|
|
const { width, height } = container.getBoundingClientRect();
|
|
if (width === 0 || height === 0) return;
|
|
|
|
graph = new Graph({
|
|
container,
|
|
width,
|
|
height,
|
|
autoFit: 'view',
|
|
animation: false,
|
|
data: graphData,
|
|
node: {
|
|
style: {
|
|
labelText: (datum) =>
|
|
(datum.data as MindMapNodeData | undefined)?.name ?? datum.id,
|
|
labelFill: textPrimary ? `rgb(${textPrimary})` : '#262626',
|
|
fill: accentPrimary ? `rgb(${accentPrimary})` : '#00beb4',
|
|
labelBackground: true,
|
|
labelBackgroundFill: 'transparent',
|
|
labelPlacement: 'top',
|
|
},
|
|
},
|
|
edge: {
|
|
type: 'cubic-horizontal',
|
|
style: {
|
|
stroke: textSecondary ? `rgb(${textSecondary})` : '#75787a',
|
|
},
|
|
},
|
|
layout: {
|
|
type: 'mindmap',
|
|
direction: 'H',
|
|
preLayout: false,
|
|
getHeight: () => 32,
|
|
getWidth: () => 120,
|
|
getVGap: () => 10,
|
|
getHGap: () => 80,
|
|
},
|
|
behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],
|
|
});
|
|
|
|
const handleNodeClick = (evt: IElementEvent) => {
|
|
const nodeId = evt.target.id as string;
|
|
const nodeData = graph!.getNodeData(nodeId);
|
|
const data = nodeData.data as MindMapNodeData | undefined;
|
|
if (data?.source_chunk_ids?.length) {
|
|
const payload: MindMapNodeValue = {
|
|
id: nodeId,
|
|
name: data.name || nodeId,
|
|
source_chunk_ids: data.source_chunk_ids,
|
|
};
|
|
onNodeClick?.(payload);
|
|
}
|
|
};
|
|
|
|
graph.on<IElementEvent>(NodeEvent.CLICK, handleNodeClick);
|
|
graphRef.current = graph;
|
|
|
|
void graph.render().then(() => {
|
|
setLoading(false);
|
|
});
|
|
};
|
|
|
|
// Defer graph creation so the browser paints the skeleton before the
|
|
// heavy G6 layout + render starts.
|
|
const rafId = requestAnimationFrame(() => {
|
|
observer = new ResizeObserver(() => {
|
|
if (!graph) {
|
|
createGraph();
|
|
return;
|
|
}
|
|
|
|
const { width, height } = container.getBoundingClientRect();
|
|
if (width > 0 && height > 0) {
|
|
graph.resize(width, height);
|
|
}
|
|
});
|
|
|
|
observer.observe(container);
|
|
createGraph();
|
|
});
|
|
|
|
return () => {
|
|
cancelAnimationFrame(rafId);
|
|
observer?.disconnect();
|
|
graph?.destroy();
|
|
graphRef.current = null;
|
|
};
|
|
}, [graphData, onNodeClick]);
|
|
|
|
return (
|
|
<div className="relative flex-1 min-h-0 h-full">
|
|
{loading && (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<SkeletonCard className="w-80" />
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={containerRef}
|
|
className={cn('h-full', !show && 'hidden', loading && 'invisible')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const MemoizedMindMapG6Graph = memo(MindMapG6Graph) as typeof MindMapG6Graph;
|
|
|
|
export { MemoizedMindMapG6Graph as MindMapG6Graph };
|
|
export default MemoizedMindMapG6Graph;
|