Merge pull request #1274 from danielaskdd/merge-node-color

Merge PR #1265 and resolve conflicts and problems
This commit is contained in:
Daniel.y
2025-04-05 16:00:22 +08:00
committed by GitHub
15 changed files with 292 additions and 144 deletions

View File

@@ -1 +1 @@
__api_version__ = "0135" __api_version__ = "0136"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-CaPD9lR_.js"></script> <script type="module" crossorigin src="/webui/assets/index-Cma7xY0-.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-f0HMqdqP.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-QU59h9JG.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -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<LegendProps> = ({ className }) => {
const { t } = useTranslation()
const typeColorMap = useGraphStore.use.typeColorMap()
if (!typeColorMap || typeColorMap.size === 0) {
return null
}
return (
<Card className={`p-2 max-w-xs ${className}`}>
<h3 className="text-sm font-medium mb-2">{t('graphPanel.legend')}</h3>
<ScrollArea className="max-h-40">
<div className="flex flex-col gap-1">
{Array.from(typeColorMap.entries()).map(([type, color]) => (
<div key={type} className="flex items-center gap-2">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-xs truncate" title={type}>
{type}
</span>
</div>
))}
</div>
</ScrollArea>
</Card>
)
}
export default Legend

View File

@@ -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 (
<Button
variant={controlButtonVariant}
onClick={toggleLegend}
tooltip={t('graphPanel.sideBar.legendControl.toggleLegend')}
size="icon"
>
<BookOpenIcon />
</Button>
)
}
export default LegendButton

View File

@@ -18,6 +18,8 @@ import GraphSearch from '@/components/graph/GraphSearch'
import GraphLabels from '@/components/graph/GraphLabels' import GraphLabels from '@/components/graph/GraphLabels'
import PropertiesView from '@/components/graph/PropertiesView' import PropertiesView from '@/components/graph/PropertiesView'
import SettingsDisplay from '@/components/graph/SettingsDisplay' import SettingsDisplay from '@/components/graph/SettingsDisplay'
import Legend from '@/components/graph/Legend'
import LegendButton from '@/components/graph/LegendButton'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
@@ -116,6 +118,7 @@ const GraphViewer = () => {
const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
const showLegend = useSettingsStore.use.showLegend()
// Initialize sigma settings once on component mount // Initialize sigma settings once on component mount
// All dynamic settings will be updated in GraphControl using useSetSettings // All dynamic settings will be updated in GraphControl using useSetSettings
@@ -195,6 +198,7 @@ const GraphViewer = () => {
<LayoutsControl /> <LayoutsControl />
<ZoomControl /> <ZoomControl />
<FullScreenControl /> <FullScreenControl />
<LegendButton />
<Settings /> <Settings />
{/* <ThemeToggle /> */} {/* <ThemeToggle /> */}
</div> </div>
@@ -205,6 +209,12 @@ const GraphViewer = () => {
</div> </div>
)} )}
{showLegend && (
<div className="absolute bottom-10 right-2">
<Legend className="bg-background/60 backdrop-blur-lg" />
</div>
)}
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2"> {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
<MiniMap width="100px" height="100px" /> <MiniMap width="100px" height="100px" />
</div> */} </div> */}

View File

@@ -11,6 +11,35 @@ import { useSettingsStore } from '@/stores/settings'
import seedrandom from 'seedrandom' 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) => { const validateGraph = (graph: RawGraph) => {
// Check if graph exists // Check if graph exists
if (!graph) { if (!graph) {
@@ -112,9 +141,6 @@ const fetchGraph = async (label: string, maxDepth: number, maxNodes: number) =>
const node = rawData.nodes[i] const node = rawData.nodes[i]
nodeIdMap[node.id] = 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.x = Math.random()
node.y = Math.random() node.y = Math.random()
node.degree = 0 node.degree = 0
@@ -264,6 +290,7 @@ const useLightrangeGraph = () => {
const nodeToExpand = useGraphStore.use.nodeToExpand() const nodeToExpand = useGraphStore.use.nodeToExpand()
const nodeToPrune = useGraphStore.use.nodeToPrune() const nodeToPrune = useGraphStore.use.nodeToPrune()
// Use ref to track if data has been loaded and initial load // Use ref to track if data has been loaded and initial load
const dataLoadedRef = useRef(false) const dataLoadedRef = useRef(false)
const initialLoadRef = useRef(false) const initialLoadRef = useRef(false)
@@ -336,7 +363,7 @@ const useLightrangeGraph = () => {
const currentMaxNodes = maxNodes const currentMaxNodes = maxNodes
// Declare a variable to store data promise // Declare a variable to store data promise
let dataPromise; let dataPromise: Promise<{ rawGraph: RawGraph | null; is_truncated: boolean | undefined } | null>;
// 1. If query label is not empty, use fetchGraph // 1. If query label is not empty, use fetchGraph
if (currentQueryLabel) { if (currentQueryLabel) {
@@ -352,7 +379,15 @@ const useLightrangeGraph = () => {
const state = useGraphStore.getState() const state = useGraphStore.getState()
const data = result?.rawGraph; const data = result?.rawGraph;
// Check if data is truncated // 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);
});
}
if (result?.is_truncated) { if (result?.is_truncated) {
toast.info(t('graphPanel.dataIsTruncated', 'Graph data is truncated to Max Nodes')); toast.info(t('graphPanel.dataIsTruncated', 'Graph data is truncated to Max Nodes'));
} }

View File

@@ -142,6 +142,7 @@
"statusDialog": { "statusDialog": {
"title": "إعدادات خادم LightRAG" "title": "إعدادات خادم LightRAG"
}, },
"legend": "المفتاح",
"sideBar": { "sideBar": {
"settings": { "settings": {
"settings": "الإعدادات", "settings": "الإعدادات",
@@ -189,6 +190,9 @@
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "شاشة كاملة", "fullScreen": "شاشة كاملة",
"windowed": "نوافذ" "windowed": "نوافذ"
},
"legendControl": {
"toggleLegend": "تبديل المفتاح"
} }
}, },
"statusIndicator": { "statusIndicator": {

View File

@@ -142,6 +142,7 @@
"statusDialog": { "statusDialog": {
"title": "LightRAG Server Settings" "title": "LightRAG Server Settings"
}, },
"legend": "Legend",
"sideBar": { "sideBar": {
"settings": { "settings": {
"settings": "Settings", "settings": "Settings",
@@ -189,6 +190,9 @@
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "Full Screen", "fullScreen": "Full Screen",
"windowed": "Windowed" "windowed": "Windowed"
},
"legendControl": {
"toggleLegend": "Toggle Legend"
} }
}, },
"statusIndicator": { "statusIndicator": {

View File

@@ -142,6 +142,7 @@
"statusDialog": { "statusDialog": {
"title": "Paramètres du Serveur LightRAG" "title": "Paramètres du Serveur LightRAG"
}, },
"legend": "Légende",
"sideBar": { "sideBar": {
"settings": { "settings": {
"settings": "Paramètres", "settings": "Paramètres",
@@ -189,6 +190,9 @@
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "Plein écran", "fullScreen": "Plein écran",
"windowed": "Fenêtré" "windowed": "Fenêtré"
},
"legendControl": {
"toggleLegend": "Basculer la légende"
} }
}, },
"statusIndicator": { "statusIndicator": {

View File

@@ -142,6 +142,7 @@
"statusDialog": { "statusDialog": {
"title": "LightRAG 服务器设置" "title": "LightRAG 服务器设置"
}, },
"legend": "图例",
"sideBar": { "sideBar": {
"settings": { "settings": {
"settings": "设置", "settings": "设置",
@@ -189,6 +190,9 @@
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "全屏", "fullScreen": "全屏",
"windowed": "窗口" "windowed": "窗口"
},
"legendControl": {
"toggleLegend": "切换图例显示"
} }
}, },
"statusIndicator": { "statusIndicator": {

View File

@@ -77,6 +77,8 @@ interface GraphState {
graphIsEmpty: boolean graphIsEmpty: boolean
lastSuccessfulQueryLabel: string lastSuccessfulQueryLabel: string
typeColorMap: Map<string, string>
// Global flags to track data fetching attempts // Global flags to track data fetching attempts
graphDataFetchAttempted: boolean graphDataFetchAttempted: boolean
labelsFetchAttempted: boolean labelsFetchAttempted: boolean
@@ -136,6 +138,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
sigmaInstance: null, sigmaInstance: null,
allDatabaseLabels: ['*'], allDatabaseLabels: ['*'],
typeColorMap: new Map<string, string>(),
searchEngine: null, searchEngine: null,
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }), setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
@@ -166,7 +170,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
searchEngine: null, searchEngine: null,
moveToSelectedNode: false, moveToSelectedNode: false,
graphIsEmpty: false graphIsEmpty: false
// Do not reset lastSuccessfulQueryLabel here as it's used to track query history
}); });
}, },
@@ -199,6 +202,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }), setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
setTypeColorMap: (typeColorMap: Map<string, string>) => set({ typeColorMap }),
setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }), setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }),
resetSearchEngine: () => set({ searchEngine: null }), resetSearchEngine: () => set({ searchEngine: null }),

View File

@@ -16,6 +16,8 @@ interface SettingsState {
// Graph viewer settings // Graph viewer settings
showPropertyPanel: boolean showPropertyPanel: boolean
showNodeSearchBar: boolean showNodeSearchBar: boolean
showLegend: boolean
setShowLegend: (show: boolean) => void
showNodeLabel: boolean showNodeLabel: boolean
enableNodeDrag: boolean enableNodeDrag: boolean
@@ -74,6 +76,7 @@ const useSettingsStoreBase = create<SettingsState>()(
language: 'en', language: 'en',
showPropertyPanel: true, showPropertyPanel: true,
showNodeSearchBar: true, showNodeSearchBar: true,
showLegend: false,
showNodeLabel: true, showNodeLabel: true,
enableNodeDrag: true, enableNodeDrag: true,
@@ -158,7 +161,8 @@ const useSettingsStoreBase = create<SettingsState>()(
querySettings: { ...state.querySettings, ...settings } 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', name: 'settings-storage',