From 81355481c16ad8df118e212f78ea8f825eebb152 Mon Sep 17 00:00:00 2001 From: choizhang Date: Thu, 3 Apr 2025 22:42:13 +0800 Subject: [PATCH] feat: Add legend components and toggle buttons --- .../src/components/graph/Legend.tsx | 41 +++++++++++++++++++ .../src/components/graph/LegendButton.tsx | 32 +++++++++++++++ lightrag_webui/src/features/GraphViewer.tsx | 10 +++++ lightrag_webui/src/hooks/useLightragGraph.tsx | 27 +++++++----- lightrag_webui/src/locales/en.json | 4 ++ lightrag_webui/src/locales/zh.json | 4 ++ lightrag_webui/src/stores/graph.ts | 7 +++- lightrag_webui/src/stores/settings.ts | 6 ++- 8 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 lightrag_webui/src/components/graph/Legend.tsx create mode 100644 lightrag_webui/src/components/graph/LegendButton.tsx diff --git a/lightrag_webui/src/components/graph/Legend.tsx b/lightrag_webui/src/components/graph/Legend.tsx new file mode 100644 index 00000000..b0b59de8 --- /dev/null +++ b/lightrag_webui/src/components/graph/Legend.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useGraphStore } from '@/stores/graph' +import { Card } from '@/components/ui/Card' +import { ScrollArea } from '@/components/ui/ScrollArea' + +interface LegendProps { + className?: string +} + +const Legend: React.FC = ({ className }) => { + const { t } = useTranslation() + const typeColorMap = useGraphStore.use.typeColorMap() + + if (!typeColorMap || typeColorMap.size === 0) { + return null + } + + return ( + +

{t('graphPanel.legend')}

+ +
+ {Array.from(typeColorMap.entries()).map(([type, color]) => ( +
+
+ + {type} + +
+ ))} +
+ + + ) +} + +export default Legend diff --git a/lightrag_webui/src/components/graph/LegendButton.tsx b/lightrag_webui/src/components/graph/LegendButton.tsx new file mode 100644 index 00000000..cf036721 --- /dev/null +++ b/lightrag_webui/src/components/graph/LegendButton.tsx @@ -0,0 +1,32 @@ +import { useCallback } from 'react' +import { BookOpenIcon } from 'lucide-react' +import Button from '@/components/ui/Button' +import { controlButtonVariant } from '@/lib/constants' +import { useSettingsStore } from '@/stores/settings' +import { useTranslation } from 'react-i18next' + +/** + * Component that toggles legend visibility. + */ +const LegendButton = () => { + const { t } = useTranslation() + const showLegend = useSettingsStore.use.showLegend() + const setShowLegend = useSettingsStore.use.setShowLegend() + + const toggleLegend = useCallback(() => { + setShowLegend(!showLegend) + }, [showLegend, setShowLegend]) + + return ( + + ) +} + +export default LegendButton diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index 9d46af55..aa32af29 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -18,6 +18,8 @@ import GraphSearch from '@/components/graph/GraphSearch' import GraphLabels from '@/components/graph/GraphLabels' import PropertiesView from '@/components/graph/PropertiesView' import SettingsDisplay from '@/components/graph/SettingsDisplay' +import Legend from '@/components/graph/Legend' +import LegendButton from '@/components/graph/LegendButton' import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' @@ -116,6 +118,7 @@ const GraphViewer = () => { const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() const enableNodeDrag = useSettingsStore.use.enableNodeDrag() + const showLegend = useSettingsStore.use.showLegend() // Initialize sigma settings once on component mount // All dynamic settings will be updated in GraphControl using useSetSettings @@ -195,6 +198,7 @@ const GraphViewer = () => { + {/* */}
@@ -205,6 +209,12 @@ const GraphViewer = () => { )} + {showLegend && ( +
+ +
+ )} + {/*
*/} diff --git a/lightrag_webui/src/hooks/useLightragGraph.tsx b/lightrag_webui/src/hooks/useLightragGraph.tsx index 06e71998..2638661c 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -12,22 +12,31 @@ import { useSettingsStore } from '@/stores/settings' import seedrandom from 'seedrandom' // Helper function to generate a color based on type -const getNodeColorByType = (nodeType: string | undefined, typeColorMap: React.RefObject>): string => { +const getNodeColorByType = (nodeType: string | undefined): string => { const defaultColor = '#CCCCCC'; // Default color for nodes without a type or undefined type if (!nodeType) { return defaultColor; } - if (!typeColorMap.current.has(nodeType)) { + + const typeColorMap = useGraphStore.getState().typeColorMap; + + if (!typeColorMap.has(nodeType)) { // Generate a color based on the type string itself for consistency // Seed the global random number generator based on the node type seedrandom(nodeType, { global: true }); // Call randomColor without arguments; it will use the globally seeded Math.random() const newColor = randomColor(); - typeColorMap.current.set(nodeType, newColor); + + const newMap = new Map(typeColorMap); + newMap.set(nodeType, newColor); + useGraphStore.setState({ typeColorMap: newMap }); + + return newColor; } + // Restore the default random seed if necessary, though usually not required for this use case // seedrandom(Date.now().toString(), { global: true }); - return typeColorMap.current.get(nodeType) || defaultColor; // Add fallback just in case + return typeColorMap.get(nodeType) || defaultColor; // Add fallback just in case }; @@ -240,8 +249,6 @@ const useLightrangeGraph = () => { const nodeToExpand = useGraphStore.use.nodeToExpand() const nodeToPrune = useGraphStore.use.nodeToPrune() - // Ref to store the mapping from node type to color - const typeColorMap = useRef>(new Map()); // Use ref to track if data has been loaded and initial load const dataLoadedRef = useRef(false) @@ -333,7 +340,7 @@ const useLightrangeGraph = () => { data.nodes.forEach(node => { // Use entity_type instead of type const nodeEntityType = node.properties?.entity_type as string | undefined; - node.color = getNodeColorByType(nodeEntityType, typeColorMap); + node.color = getNodeColorByType(nodeEntityType); }); } @@ -446,9 +453,9 @@ const useLightrangeGraph = () => { // Process nodes to add required properties for RawNodeType const processedNodes: RawNodeType[] = []; for (const node of extendedGraph.nodes) { - // Get color based on entity_type using the helper function and the shared map - const nodeEntityType = node.properties?.entity_type as string | undefined; // Use entity_type - const color = getNodeColorByType(nodeEntityType, typeColorMap); + // Get color based on entity_type using the helper function + const nodeEntityType = node.properties?.entity_type as string | undefined; + const color = getNodeColorByType(nodeEntityType); // Create a properly typed RawNodeType processedNodes.push({ diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index d7c68c73..177c8eae 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -127,6 +127,7 @@ } }, "graphPanel": { + "legend": "Legend", "sideBar": { "settings": { "settings": "Settings", @@ -171,6 +172,9 @@ "fullScreenControl": { "fullScreen": "Full Screen", "windowed": "Windowed" + }, + "legendControl": { + "toggleLegend": "Toggle Legend" } }, "statusIndicator": { diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index bd1a1841..cd6adbdb 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -127,6 +127,7 @@ } }, "graphPanel": { + "legend": "图例", "sideBar": { "settings": { "settings": "设置", @@ -171,6 +172,9 @@ "fullScreenControl": { "fullScreen": "全屏", "windowed": "窗口" + }, + "legendControl": { + "toggleLegend": "切换图例显示" } }, "statusIndicator": { diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index cfcebc1c..c50cff41 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -77,6 +77,8 @@ interface GraphState { graphIsEmpty: boolean lastSuccessfulQueryLabel: string + typeColorMap: Map + // Global flags to track data fetching attempts graphDataFetchAttempted: boolean labelsFetchAttempted: boolean @@ -136,6 +138,8 @@ const useGraphStoreBase = create()((set) => ({ sigmaInstance: null, allDatabaseLabels: ['*'], + typeColorMap: new Map(), + searchEngine: null, setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }), @@ -166,7 +170,6 @@ const useGraphStoreBase = create()((set) => ({ searchEngine: null, moveToSelectedNode: false, graphIsEmpty: false - // Do not reset lastSuccessfulQueryLabel here as it's used to track query history }); }, @@ -199,6 +202,8 @@ const useGraphStoreBase = create()((set) => ({ setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }), + setTypeColorMap: (typeColorMap: Map) => set({ typeColorMap }), + setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }), resetSearchEngine: () => set({ searchEngine: null }), diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts index 311e5061..cfac0551 100644 --- a/lightrag_webui/src/stores/settings.ts +++ b/lightrag_webui/src/stores/settings.ts @@ -16,6 +16,8 @@ interface SettingsState { // Graph viewer settings showPropertyPanel: boolean showNodeSearchBar: boolean + showLegend: boolean + setShowLegend: (show: boolean) => void showNodeLabel: boolean enableNodeDrag: boolean @@ -68,6 +70,7 @@ const useSettingsStoreBase = create()( language: 'en', showPropertyPanel: true, showNodeSearchBar: true, + showLegend: false, showNodeLabel: true, enableNodeDrag: true, @@ -145,7 +148,8 @@ const useSettingsStoreBase = create()( querySettings: { ...state.querySettings, ...settings } })), - setShowFileName: (show: boolean) => set({ showFileName: show }) + setShowFileName: (show: boolean) => set({ showFileName: show }), + setShowLegend: (show: boolean) => set({ showLegend: show }) }), { name: 'settings-storage',