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 bc5f526b..a47ca060 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -11,6 +11,35 @@ import { useSettingsStore } from '@/stores/settings' import seedrandom from 'seedrandom' +// Helper function to generate a color based on type +const getNodeColorByType = (nodeType: string | undefined): string => { + const defaultColor = '#CCCCCC'; // Default color for nodes without a type or undefined type + if (!nodeType) { + return defaultColor; + } + + 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(); + + 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.get(nodeType) || defaultColor; // Add fallback just in case +}; + + const validateGraph = (graph: RawGraph) => { // Check if graph exists if (!graph) { @@ -112,9 +141,6 @@ const fetchGraph = async (label: string, maxDepth: number, maxNodes: number) => const node = rawData.nodes[i] nodeIdMap[node.id] = i - // const seed = node.labels.length > 0 ? node.labels[0] : node.id - seedrandom(node.id, { global: true }) - node.color = randomColor() node.x = Math.random() node.y = Math.random() node.degree = 0 @@ -264,6 +290,7 @@ const useLightrangeGraph = () => { const nodeToExpand = useGraphStore.use.nodeToExpand() const nodeToPrune = useGraphStore.use.nodeToPrune() + // Use ref to track if data has been loaded and initial load const dataLoadedRef = useRef(false) const initialLoadRef = useRef(false) @@ -336,7 +363,7 @@ const useLightrangeGraph = () => { const currentMaxNodes = maxNodes // Declare a variable to store data promise - let dataPromise; + let dataPromise: Promise; // 1. If query label is not empty, use fetchGraph if (currentQueryLabel) { @@ -348,7 +375,16 @@ const useLightrangeGraph = () => { } // 3. Process data - dataPromise.then((result) => { + dataPromise.then((data) => { + // Assign colors based on entity_type *after* fetching + if (data && data.nodes) { + data.nodes.forEach(node => { + // Use entity_type instead of type + const nodeEntityType = node.properties?.entity_type as string | undefined; + node.color = getNodeColorByType(nodeEntityType); + }); + } + const state = useGraphStore.getState() const data = result?.rawGraph; @@ -472,9 +508,9 @@ const useLightrangeGraph = () => { // Process nodes to add required properties for RawNodeType const processedNodes: RawNodeType[] = []; for (const node of extendedGraph.nodes) { - // Generate random color values - seedrandom(node.id, { global: true }); - const color = randomColor(); + // 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/lib/constants.ts b/lightrag_webui/src/lib/constants.ts index 048ae8f7..87db8cea 100644 --- a/lightrag_webui/src/lib/constants.ts +++ b/lightrag_webui/src/lib/constants.ts @@ -1,6 +1,6 @@ import { ButtonVariantType } from '@/components/ui/Button' -export const backendBaseUrl = '' +export const backendBaseUrl = 'http://localhost:9621' export const webuiPrefix = '/webui/' export const controlButtonVariant: ButtonVariantType = 'ghost' diff --git a/lightrag_webui/src/locales/ar.json b/lightrag_webui/src/locales/ar.json index 73475cea..906fe58a 100644 --- a/lightrag_webui/src/locales/ar.json +++ b/lightrag_webui/src/locales/ar.json @@ -142,6 +142,7 @@ "statusDialog": { "title": "إعدادات خادم LightRAG" }, + "legend": "المفتاح", "sideBar": { "settings": { "settings": "الإعدادات", @@ -189,6 +190,9 @@ "fullScreenControl": { "fullScreen": "شاشة كاملة", "windowed": "نوافذ" + }, + "legendControl": { + "toggleLegend": "تبديل المفتاح" } }, "statusIndicator": { diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index aa740e64..2e296a4d 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -142,6 +142,7 @@ "statusDialog": { "title": "LightRAG Server Settings" }, + "legend": "Legend", "sideBar": { "settings": { "settings": "Settings", @@ -189,6 +190,9 @@ "fullScreenControl": { "fullScreen": "Full Screen", "windowed": "Windowed" + }, + "legendControl": { + "toggleLegend": "Toggle Legend" } }, "statusIndicator": { diff --git a/lightrag_webui/src/locales/fr.json b/lightrag_webui/src/locales/fr.json index 2fbf1b6e..b2067d70 100644 --- a/lightrag_webui/src/locales/fr.json +++ b/lightrag_webui/src/locales/fr.json @@ -142,6 +142,7 @@ "statusDialog": { "title": "Paramètres du Serveur LightRAG" }, + "legend": "Légende", "sideBar": { "settings": { "settings": "Paramètres", @@ -189,6 +190,9 @@ "fullScreenControl": { "fullScreen": "Plein écran", "windowed": "Fenêtré" + }, + "legendControl": { + "toggleLegend": "Basculer la légende" } }, "statusIndicator": { diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index 88def36a..931fb659 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -142,6 +142,7 @@ "statusDialog": { "title": "LightRAG 服务器设置" }, + "legend": "图例", "sideBar": { "settings": { "settings": "设置", @@ -189,6 +190,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 74513d48..2c314e06 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 @@ -74,6 +76,7 @@ const useSettingsStoreBase = create()( language: 'en', showPropertyPanel: true, showNodeSearchBar: true, + showLegend: false, showNodeLabel: true, enableNodeDrag: true, @@ -158,7 +161,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',