yangdx commited on
Commit
0e483e2
·
2 Parent(s): a209457 cb31da4

Merge branch 'feat-node-color' into merge-node-color

Browse files
lightrag_webui/src/components/graph/Legend.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { useGraphStore } from '@/stores/graph'
4
+ import { Card } from '@/components/ui/Card'
5
+ import { ScrollArea } from '@/components/ui/ScrollArea'
6
+
7
+ interface LegendProps {
8
+ className?: string
9
+ }
10
+
11
+ const Legend: React.FC<LegendProps> = ({ className }) => {
12
+ const { t } = useTranslation()
13
+ const typeColorMap = useGraphStore.use.typeColorMap()
14
+
15
+ if (!typeColorMap || typeColorMap.size === 0) {
16
+ return null
17
+ }
18
+
19
+ return (
20
+ <Card className={`p-2 max-w-xs ${className}`}>
21
+ <h3 className="text-sm font-medium mb-2">{t('graphPanel.legend')}</h3>
22
+ <ScrollArea className="max-h-40">
23
+ <div className="flex flex-col gap-1">
24
+ {Array.from(typeColorMap.entries()).map(([type, color]) => (
25
+ <div key={type} className="flex items-center gap-2">
26
+ <div
27
+ className="w-4 h-4 rounded-full"
28
+ style={{ backgroundColor: color }}
29
+ />
30
+ <span className="text-xs truncate" title={type}>
31
+ {type}
32
+ </span>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ </ScrollArea>
37
+ </Card>
38
+ )
39
+ }
40
+
41
+ export default Legend
lightrag_webui/src/components/graph/LegendButton.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react'
2
+ import { BookOpenIcon } from 'lucide-react'
3
+ import Button from '@/components/ui/Button'
4
+ import { controlButtonVariant } from '@/lib/constants'
5
+ import { useSettingsStore } from '@/stores/settings'
6
+ import { useTranslation } from 'react-i18next'
7
+
8
+ /**
9
+ * Component that toggles legend visibility.
10
+ */
11
+ const LegendButton = () => {
12
+ const { t } = useTranslation()
13
+ const showLegend = useSettingsStore.use.showLegend()
14
+ const setShowLegend = useSettingsStore.use.setShowLegend()
15
+
16
+ const toggleLegend = useCallback(() => {
17
+ setShowLegend(!showLegend)
18
+ }, [showLegend, setShowLegend])
19
+
20
+ return (
21
+ <Button
22
+ variant={controlButtonVariant}
23
+ onClick={toggleLegend}
24
+ tooltip={t('graphPanel.sideBar.legendControl.toggleLegend')}
25
+ size="icon"
26
+ >
27
+ <BookOpenIcon />
28
+ </Button>
29
+ )
30
+ }
31
+
32
+ export default LegendButton
lightrag_webui/src/features/GraphViewer.tsx CHANGED
@@ -18,6 +18,8 @@ import GraphSearch from '@/components/graph/GraphSearch'
18
  import GraphLabels from '@/components/graph/GraphLabels'
19
  import PropertiesView from '@/components/graph/PropertiesView'
20
  import SettingsDisplay from '@/components/graph/SettingsDisplay'
 
 
21
 
22
  import { useSettingsStore } from '@/stores/settings'
23
  import { useGraphStore } from '@/stores/graph'
@@ -116,6 +118,7 @@ const GraphViewer = () => {
116
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
117
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
118
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
 
119
 
120
  // Initialize sigma settings once on component mount
121
  // All dynamic settings will be updated in GraphControl using useSetSettings
@@ -195,6 +198,7 @@ const GraphViewer = () => {
195
  <LayoutsControl />
196
  <ZoomControl />
197
  <FullScreenControl />
 
198
  <Settings />
199
  {/* <ThemeToggle /> */}
200
  </div>
@@ -205,6 +209,12 @@ const GraphViewer = () => {
205
  </div>
206
  )}
207
 
 
 
 
 
 
 
208
  {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
209
  <MiniMap width="100px" height="100px" />
210
  </div> */}
 
18
  import GraphLabels from '@/components/graph/GraphLabels'
19
  import PropertiesView from '@/components/graph/PropertiesView'
20
  import SettingsDisplay from '@/components/graph/SettingsDisplay'
21
+ import Legend from '@/components/graph/Legend'
22
+ import LegendButton from '@/components/graph/LegendButton'
23
 
24
  import { useSettingsStore } from '@/stores/settings'
25
  import { useGraphStore } from '@/stores/graph'
 
118
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
119
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
120
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
121
+ const showLegend = useSettingsStore.use.showLegend()
122
 
123
  // Initialize sigma settings once on component mount
124
  // All dynamic settings will be updated in GraphControl using useSetSettings
 
198
  <LayoutsControl />
199
  <ZoomControl />
200
  <FullScreenControl />
201
+ <LegendButton />
202
  <Settings />
203
  {/* <ThemeToggle /> */}
204
  </div>
 
209
  </div>
210
  )}
211
 
212
+ {showLegend && (
213
+ <div className="absolute bottom-10 left-15">
214
+ <Legend className="bg-background/60 backdrop-blur-lg" />
215
+ </div>
216
+ )}
217
+
218
  {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
219
  <MiniMap width="100px" height="100px" />
220
  </div> */}
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -11,6 +11,35 @@ import { useSettingsStore } from '@/stores/settings'
11
 
12
  import seedrandom from 'seedrandom'
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  const validateGraph = (graph: RawGraph) => {
15
  // Check if graph exists
16
  if (!graph) {
@@ -112,9 +141,6 @@ const fetchGraph = async (label: string, maxDepth: number, maxNodes: number) =>
112
  const node = rawData.nodes[i]
113
  nodeIdMap[node.id] = i
114
 
115
- // const seed = node.labels.length > 0 ? node.labels[0] : node.id
116
- seedrandom(node.id, { global: true })
117
- node.color = randomColor()
118
  node.x = Math.random()
119
  node.y = Math.random()
120
  node.degree = 0
@@ -264,6 +290,7 @@ const useLightrangeGraph = () => {
264
  const nodeToExpand = useGraphStore.use.nodeToExpand()
265
  const nodeToPrune = useGraphStore.use.nodeToPrune()
266
 
 
267
  // Use ref to track if data has been loaded and initial load
268
  const dataLoadedRef = useRef(false)
269
  const initialLoadRef = useRef(false)
@@ -336,7 +363,7 @@ const useLightrangeGraph = () => {
336
  const currentMaxNodes = maxNodes
337
 
338
  // Declare a variable to store data promise
339
- let dataPromise;
340
 
341
  // 1. If query label is not empty, use fetchGraph
342
  if (currentQueryLabel) {
@@ -348,7 +375,16 @@ const useLightrangeGraph = () => {
348
  }
349
 
350
  // 3. Process data
351
- dataPromise.then((result) => {
 
 
 
 
 
 
 
 
 
352
  const state = useGraphStore.getState()
353
  const data = result?.rawGraph;
354
 
@@ -472,9 +508,9 @@ const useLightrangeGraph = () => {
472
  // Process nodes to add required properties for RawNodeType
473
  const processedNodes: RawNodeType[] = [];
474
  for (const node of extendedGraph.nodes) {
475
- // Generate random color values
476
- seedrandom(node.id, { global: true });
477
- const color = randomColor();
478
 
479
  // Create a properly typed RawNodeType
480
  processedNodes.push({
 
11
 
12
  import seedrandom from 'seedrandom'
13
 
14
+ // Helper function to generate a color based on type
15
+ const getNodeColorByType = (nodeType: string | undefined): string => {
16
+ const defaultColor = '#CCCCCC'; // Default color for nodes without a type or undefined type
17
+ if (!nodeType) {
18
+ return defaultColor;
19
+ }
20
+
21
+ const typeColorMap = useGraphStore.getState().typeColorMap;
22
+
23
+ if (!typeColorMap.has(nodeType)) {
24
+ // Generate a color based on the type string itself for consistency
25
+ // Seed the global random number generator based on the node type
26
+ seedrandom(nodeType, { global: true });
27
+ // Call randomColor without arguments; it will use the globally seeded Math.random()
28
+ const newColor = randomColor();
29
+
30
+ const newMap = new Map(typeColorMap);
31
+ newMap.set(nodeType, newColor);
32
+ useGraphStore.setState({ typeColorMap: newMap });
33
+
34
+ return newColor;
35
+ }
36
+
37
+ // Restore the default random seed if necessary, though usually not required for this use case
38
+ // seedrandom(Date.now().toString(), { global: true });
39
+ return typeColorMap.get(nodeType) || defaultColor; // Add fallback just in case
40
+ };
41
+
42
+
43
  const validateGraph = (graph: RawGraph) => {
44
  // Check if graph exists
45
  if (!graph) {
 
141
  const node = rawData.nodes[i]
142
  nodeIdMap[node.id] = i
143
 
 
 
 
144
  node.x = Math.random()
145
  node.y = Math.random()
146
  node.degree = 0
 
290
  const nodeToExpand = useGraphStore.use.nodeToExpand()
291
  const nodeToPrune = useGraphStore.use.nodeToPrune()
292
 
293
+
294
  // Use ref to track if data has been loaded and initial load
295
  const dataLoadedRef = useRef(false)
296
  const initialLoadRef = useRef(false)
 
363
  const currentMaxNodes = maxNodes
364
 
365
  // Declare a variable to store data promise
366
+ let dataPromise: Promise<RawGraph | null>;
367
 
368
  // 1. If query label is not empty, use fetchGraph
369
  if (currentQueryLabel) {
 
375
  }
376
 
377
  // 3. Process data
378
+ dataPromise.then((data) => {
379
+ // Assign colors based on entity_type *after* fetching
380
+ if (data && data.nodes) {
381
+ data.nodes.forEach(node => {
382
+ // Use entity_type instead of type
383
+ const nodeEntityType = node.properties?.entity_type as string | undefined;
384
+ node.color = getNodeColorByType(nodeEntityType);
385
+ });
386
+ }
387
+
388
  const state = useGraphStore.getState()
389
  const data = result?.rawGraph;
390
 
 
508
  // Process nodes to add required properties for RawNodeType
509
  const processedNodes: RawNodeType[] = [];
510
  for (const node of extendedGraph.nodes) {
511
+ // Get color based on entity_type using the helper function
512
+ const nodeEntityType = node.properties?.entity_type as string | undefined;
513
+ const color = getNodeColorByType(nodeEntityType);
514
 
515
  // Create a properly typed RawNodeType
516
  processedNodes.push({
lightrag_webui/src/lib/constants.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
- export const backendBaseUrl = ''
4
  export const webuiPrefix = '/webui/'
5
 
6
  export const controlButtonVariant: ButtonVariantType = 'ghost'
 
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
+ export const backendBaseUrl = 'http://localhost:9621'
4
  export const webuiPrefix = '/webui/'
5
 
6
  export const controlButtonVariant: ButtonVariantType = 'ghost'
lightrag_webui/src/locales/ar.json CHANGED
@@ -142,6 +142,7 @@
142
  "statusDialog": {
143
  "title": "إعدادات خادم LightRAG"
144
  },
 
145
  "sideBar": {
146
  "settings": {
147
  "settings": "الإعدادات",
@@ -189,6 +190,9 @@
189
  "fullScreenControl": {
190
  "fullScreen": "شاشة كاملة",
191
  "windowed": "نوافذ"
 
 
 
192
  }
193
  },
194
  "statusIndicator": {
 
142
  "statusDialog": {
143
  "title": "إعدادات خادم LightRAG"
144
  },
145
+ "legend": "المفتاح",
146
  "sideBar": {
147
  "settings": {
148
  "settings": "الإعدادات",
 
190
  "fullScreenControl": {
191
  "fullScreen": "شاشة كاملة",
192
  "windowed": "نوافذ"
193
+ },
194
+ "legendControl": {
195
+ "toggleLegend": "تبديل المفتاح"
196
  }
197
  },
198
  "statusIndicator": {
lightrag_webui/src/locales/en.json CHANGED
@@ -142,6 +142,7 @@
142
  "statusDialog": {
143
  "title": "LightRAG Server Settings"
144
  },
 
145
  "sideBar": {
146
  "settings": {
147
  "settings": "Settings",
@@ -189,6 +190,9 @@
189
  "fullScreenControl": {
190
  "fullScreen": "Full Screen",
191
  "windowed": "Windowed"
 
 
 
192
  }
193
  },
194
  "statusIndicator": {
 
142
  "statusDialog": {
143
  "title": "LightRAG Server Settings"
144
  },
145
+ "legend": "Legend",
146
  "sideBar": {
147
  "settings": {
148
  "settings": "Settings",
 
190
  "fullScreenControl": {
191
  "fullScreen": "Full Screen",
192
  "windowed": "Windowed"
193
+ },
194
+ "legendControl": {
195
+ "toggleLegend": "Toggle Legend"
196
  }
197
  },
198
  "statusIndicator": {
lightrag_webui/src/locales/fr.json CHANGED
@@ -142,6 +142,7 @@
142
  "statusDialog": {
143
  "title": "Paramètres du Serveur LightRAG"
144
  },
 
145
  "sideBar": {
146
  "settings": {
147
  "settings": "Paramètres",
@@ -189,6 +190,9 @@
189
  "fullScreenControl": {
190
  "fullScreen": "Plein écran",
191
  "windowed": "Fenêtré"
 
 
 
192
  }
193
  },
194
  "statusIndicator": {
 
142
  "statusDialog": {
143
  "title": "Paramètres du Serveur LightRAG"
144
  },
145
+ "legend": "Légende",
146
  "sideBar": {
147
  "settings": {
148
  "settings": "Paramètres",
 
190
  "fullScreenControl": {
191
  "fullScreen": "Plein écran",
192
  "windowed": "Fenêtré"
193
+ },
194
+ "legendControl": {
195
+ "toggleLegend": "Basculer la légende"
196
  }
197
  },
198
  "statusIndicator": {
lightrag_webui/src/locales/zh.json CHANGED
@@ -142,6 +142,7 @@
142
  "statusDialog": {
143
  "title": "LightRAG 服务器设置"
144
  },
 
145
  "sideBar": {
146
  "settings": {
147
  "settings": "设置",
@@ -189,6 +190,9 @@
189
  "fullScreenControl": {
190
  "fullScreen": "全屏",
191
  "windowed": "窗口"
 
 
 
192
  }
193
  },
194
  "statusIndicator": {
 
142
  "statusDialog": {
143
  "title": "LightRAG 服务器设置"
144
  },
145
+ "legend": "图例",
146
  "sideBar": {
147
  "settings": {
148
  "settings": "设置",
 
190
  "fullScreenControl": {
191
  "fullScreen": "全屏",
192
  "windowed": "窗口"
193
+ },
194
+ "legendControl": {
195
+ "toggleLegend": "切换图例显示"
196
  }
197
  },
198
  "statusIndicator": {
lightrag_webui/src/stores/graph.ts CHANGED
@@ -77,6 +77,8 @@ interface GraphState {
77
  graphIsEmpty: boolean
78
  lastSuccessfulQueryLabel: string
79
 
 
 
80
  // Global flags to track data fetching attempts
81
  graphDataFetchAttempted: boolean
82
  labelsFetchAttempted: boolean
@@ -136,6 +138,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
136
  sigmaInstance: null,
137
  allDatabaseLabels: ['*'],
138
 
 
 
139
  searchEngine: null,
140
 
141
  setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
@@ -166,7 +170,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
166
  searchEngine: null,
167
  moveToSelectedNode: false,
168
  graphIsEmpty: false
169
- // Do not reset lastSuccessfulQueryLabel here as it's used to track query history
170
  });
171
  },
172
 
@@ -199,6 +202,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
199
 
200
  setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
201
 
 
 
202
  setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }),
203
  resetSearchEngine: () => set({ searchEngine: null }),
204
 
 
77
  graphIsEmpty: boolean
78
  lastSuccessfulQueryLabel: string
79
 
80
+ typeColorMap: Map<string, string>
81
+
82
  // Global flags to track data fetching attempts
83
  graphDataFetchAttempted: boolean
84
  labelsFetchAttempted: boolean
 
138
  sigmaInstance: null,
139
  allDatabaseLabels: ['*'],
140
 
141
+ typeColorMap: new Map<string, string>(),
142
+
143
  searchEngine: null,
144
 
145
  setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
 
170
  searchEngine: null,
171
  moveToSelectedNode: false,
172
  graphIsEmpty: false
 
173
  });
174
  },
175
 
 
202
 
203
  setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
204
 
205
+ setTypeColorMap: (typeColorMap: Map<string, string>) => set({ typeColorMap }),
206
+
207
  setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }),
208
  resetSearchEngine: () => set({ searchEngine: null }),
209
 
lightrag_webui/src/stores/settings.ts CHANGED
@@ -16,6 +16,8 @@ interface SettingsState {
16
  // Graph viewer settings
17
  showPropertyPanel: boolean
18
  showNodeSearchBar: boolean
 
 
19
 
20
  showNodeLabel: boolean
21
  enableNodeDrag: boolean
@@ -74,6 +76,7 @@ const useSettingsStoreBase = create<SettingsState>()(
74
  language: 'en',
75
  showPropertyPanel: true,
76
  showNodeSearchBar: true,
 
77
 
78
  showNodeLabel: true,
79
  enableNodeDrag: true,
@@ -158,7 +161,8 @@ const useSettingsStoreBase = create<SettingsState>()(
158
  querySettings: { ...state.querySettings, ...settings }
159
  })),
160
 
161
- setShowFileName: (show: boolean) => set({ showFileName: show })
 
162
  }),
163
  {
164
  name: 'settings-storage',
 
16
  // Graph viewer settings
17
  showPropertyPanel: boolean
18
  showNodeSearchBar: boolean
19
+ showLegend: boolean
20
+ setShowLegend: (show: boolean) => void
21
 
22
  showNodeLabel: boolean
23
  enableNodeDrag: boolean
 
76
  language: 'en',
77
  showPropertyPanel: true,
78
  showNodeSearchBar: true,
79
+ showLegend: false,
80
 
81
  showNodeLabel: true,
82
  enableNodeDrag: true,
 
161
  querySettings: { ...state.querySettings, ...settings }
162
  })),
163
 
164
+ setShowFileName: (show: boolean) => set({ showFileName: show }),
165
+ setShowLegend: (show: boolean) => set({ showLegend: show })
166
  }),
167
  {
168
  name: 'settings-storage',