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 +41 -0
- lightrag_webui/src/components/graph/LegendButton.tsx +32 -0
- lightrag_webui/src/features/GraphViewer.tsx +10 -0
- lightrag_webui/src/hooks/useLightragGraph.tsx +17 -10
- lightrag_webui/src/locales/en.json +4 -0
- lightrag_webui/src/locales/zh.json +4 -0
- lightrag_webui/src/stores/graph.ts +6 -1
- lightrag_webui/src/stores/settings.ts +5 -1
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
|
16 |
const defaultColor = '#CCCCCC'; // Default color for nodes without a type or undefined type
|
17 |
if (!nodeType) {
|
18 |
return defaultColor;
|
19 |
}
|
20 |
-
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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.
|
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
|
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
|
450 |
-
const nodeEntityType = node.properties?.entity_type as string | undefined;
|
451 |
-
const color = getNodeColorByType(nodeEntityType
|
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',
|