choizhang commited on
Commit
563cdd1
·
1 Parent(s): cba1d17

feat: Add legend components and toggle buttons

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
@@ -12,22 +12,31 @@ import { useSettingsStore } from '@/stores/settings'
12
  import seedrandom from 'seedrandom'
13
 
14
  // Helper function to generate a color based on type
15
- const getNodeColorByType = (nodeType: string | undefined, typeColorMap: React.RefObject<Map<string, string>>): string => {
16
  const defaultColor = '#CCCCCC'; // Default color for nodes without a type or undefined type
17
  if (!nodeType) {
18
  return defaultColor;
19
  }
20
- if (!typeColorMap.current.has(nodeType)) {
 
 
 
21
  // Generate a color based on the type string itself for consistency
22
  // Seed the global random number generator based on the node type
23
  seedrandom(nodeType, { global: true });
24
  // Call randomColor without arguments; it will use the globally seeded Math.random()
25
  const newColor = randomColor();
26
- typeColorMap.current.set(nodeType, newColor);
 
 
 
 
 
27
  }
 
28
  // Restore the default random seed if necessary, though usually not required for this use case
29
  // seedrandom(Date.now().toString(), { global: true });
30
- return typeColorMap.current.get(nodeType) || defaultColor; // Add fallback just in case
31
  };
32
 
33
 
@@ -240,8 +249,6 @@ const useLightrangeGraph = () => {
240
  const nodeToExpand = useGraphStore.use.nodeToExpand()
241
  const nodeToPrune = useGraphStore.use.nodeToPrune()
242
 
243
- // Ref to store the mapping from node type to color
244
- const typeColorMap = useRef<Map<string, string>>(new Map());
245
 
246
  // Use ref to track if data has been loaded and initial load
247
  const dataLoadedRef = useRef(false)
@@ -333,7 +340,7 @@ const useLightrangeGraph = () => {
333
  data.nodes.forEach(node => {
334
  // Use entity_type instead of type
335
  const nodeEntityType = node.properties?.entity_type as string | undefined;
336
- node.color = getNodeColorByType(nodeEntityType, typeColorMap);
337
  });
338
  }
339
 
@@ -446,9 +453,9 @@ const useLightrangeGraph = () => {
446
  // Process nodes to add required properties for RawNodeType
447
  const processedNodes: RawNodeType[] = [];
448
  for (const node of extendedGraph.nodes) {
449
- // Get color based on entity_type using the helper function and the shared map
450
- const nodeEntityType = node.properties?.entity_type as string | undefined; // Use entity_type
451
- const color = getNodeColorByType(nodeEntityType, typeColorMap);
452
 
453
  // Create a properly typed RawNodeType
454
  processedNodes.push({
 
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
 
 
249
  const nodeToExpand = useGraphStore.use.nodeToExpand()
250
  const nodeToPrune = useGraphStore.use.nodeToPrune()
251
 
 
 
252
 
253
  // Use ref to track if data has been loaded and initial load
254
  const dataLoadedRef = useRef(false)
 
340
  data.nodes.forEach(node => {
341
  // Use entity_type instead of type
342
  const nodeEntityType = node.properties?.entity_type as string | undefined;
343
+ node.color = getNodeColorByType(nodeEntityType);
344
  });
345
  }
346
 
 
453
  // Process nodes to add required properties for RawNodeType
454
  const processedNodes: RawNodeType[] = [];
455
  for (const node of extendedGraph.nodes) {
456
+ // Get color based on entity_type using the helper function
457
+ const nodeEntityType = node.properties?.entity_type as string | undefined;
458
+ const color = getNodeColorByType(nodeEntityType);
459
 
460
  // Create a properly typed RawNodeType
461
  processedNodes.push({
lightrag_webui/src/locales/en.json CHANGED
@@ -127,6 +127,7 @@
127
  }
128
  },
129
  "graphPanel": {
 
130
  "sideBar": {
131
  "settings": {
132
  "settings": "Settings",
@@ -171,6 +172,9 @@
171
  "fullScreenControl": {
172
  "fullScreen": "Full Screen",
173
  "windowed": "Windowed"
 
 
 
174
  }
175
  },
176
  "statusIndicator": {
 
127
  }
128
  },
129
  "graphPanel": {
130
+ "legend": "Legend",
131
  "sideBar": {
132
  "settings": {
133
  "settings": "Settings",
 
172
  "fullScreenControl": {
173
  "fullScreen": "Full Screen",
174
  "windowed": "Windowed"
175
+ },
176
+ "legendControl": {
177
+ "toggleLegend": "Toggle Legend"
178
  }
179
  },
180
  "statusIndicator": {
lightrag_webui/src/locales/zh.json CHANGED
@@ -127,6 +127,7 @@
127
  }
128
  },
129
  "graphPanel": {
 
130
  "sideBar": {
131
  "settings": {
132
  "settings": "设置",
@@ -171,6 +172,9 @@
171
  "fullScreenControl": {
172
  "fullScreen": "全屏",
173
  "windowed": "窗口"
 
 
 
174
  }
175
  },
176
  "statusIndicator": {
 
127
  }
128
  },
129
  "graphPanel": {
130
+ "legend": "图例",
131
  "sideBar": {
132
  "settings": {
133
  "settings": "设置",
 
172
  "fullScreenControl": {
173
  "fullScreen": "全屏",
174
  "windowed": "窗口"
175
+ },
176
+ "legendControl": {
177
+ "toggleLegend": "切换图例显示"
178
  }
179
  },
180
  "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
@@ -68,6 +70,7 @@ const useSettingsStoreBase = create<SettingsState>()(
68
  language: 'en',
69
  showPropertyPanel: true,
70
  showNodeSearchBar: true,
 
71
 
72
  showNodeLabel: true,
73
  enableNodeDrag: true,
@@ -145,7 +148,8 @@ const useSettingsStoreBase = create<SettingsState>()(
145
  querySettings: { ...state.querySettings, ...settings }
146
  })),
147
 
148
- setShowFileName: (show: boolean) => set({ showFileName: show })
 
149
  }),
150
  {
151
  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
 
70
  language: 'en',
71
  showPropertyPanel: true,
72
  showNodeSearchBar: true,
73
+ showLegend: false,
74
 
75
  showNodeLabel: true,
76
  enableNodeDrag: true,
 
148
  querySettings: { ...state.querySettings, ...settings }
149
  })),
150
 
151
+ setShowFileName: (show: boolean) => set({ showFileName: show }),
152
+ setShowLegend: (show: boolean) => set({ showLegend: show })
153
  }),
154
  {
155
  name: 'settings-storage',