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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-CaPD9lR_.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-f0HMqdqP.css">
<script type="module" crossorigin src="/webui/assets/index-Cma7xY0-.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-QU59h9JG.css">
</head>
<body>
<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 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 = () => {
<LayoutsControl />
<ZoomControl />
<FullScreenControl />
<LegendButton />
<Settings />
{/* <ThemeToggle /> */}
</div>
@@ -205,6 +209,12 @@ const GraphViewer = () => {
</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">
<MiniMap width="100px" height="100px" />
</div> */}

View File

@@ -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<{ rawGraph: RawGraph | null; is_truncated: boolean | undefined } | null>;
// 1. If query label is not empty, use fetchGraph
if (currentQueryLabel) {
@@ -352,7 +379,15 @@ const useLightrangeGraph = () => {
const state = useGraphStore.getState()
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) {
toast.info(t('graphPanel.dataIsTruncated', 'Graph data is truncated to Max Nodes'));
}

View File

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

View File

@@ -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": {

View File

@@ -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": {

View File

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

View File

@@ -77,6 +77,8 @@ interface GraphState {
graphIsEmpty: boolean
lastSuccessfulQueryLabel: string
typeColorMap: Map<string, string>
// Global flags to track data fetching attempts
graphDataFetchAttempted: boolean
labelsFetchAttempted: boolean
@@ -136,6 +138,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
sigmaInstance: null,
allDatabaseLabels: ['*'],
typeColorMap: new Map<string, string>(),
searchEngine: null,
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
@@ -166,7 +170,6 @@ const useGraphStoreBase = create<GraphState>()((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<GraphState>()((set) => ({
setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
setTypeColorMap: (typeColorMap: Map<string, string>) => set({ typeColorMap }),
setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }),
resetSearchEngine: () => set({ searchEngine: null }),

View File

@@ -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<SettingsState>()(
language: 'en',
showPropertyPanel: true,
showNodeSearchBar: true,
showLegend: false,
showNodeLabel: true,
enableNodeDrag: true,
@@ -158,7 +161,8 @@ const useSettingsStoreBase = create<SettingsState>()(
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',