Merge pull request #2287 from danielaskdd/fix-ui

Refact: Enhance Property editing UI for KG Nodes
This commit is contained in:
Daniel.y 2025-10-31 00:23:39 +08:00 committed by GitHub
commit bda52a8773
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 253 additions and 219 deletions

View file

@ -126,9 +126,10 @@ const EditablePropertyRow = ({
} else {
// Node was updated/renamed normally
try {
const graphValue = name === 'entity_id' ? finalValue : value
await useGraphStore
.getState()
.updateNodeAndSelect(nodeId, entityId, name, finalValue)
.updateNodeAndSelect(nodeId, entityId, name, graphValue)
} catch (error) {
console.error('Error updating node in graph:', error)
throw new Error('Failed to update node in graph')

View file

@ -88,9 +88,10 @@ const PropertyEditDialog = ({
};
const handleSave = async () => {
if (value.trim() !== '') {
const trimmedValue = value.trim()
if (trimmedValue !== '') {
const options = propertyName === 'entity_id' ? { allowMerge } : undefined
await onSave(value, options)
await onSave(trimmedValue, options)
}
}

View file

@ -10,225 +10,18 @@ import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import seedrandom from 'seedrandom'
const TYPE_SYNONYMS: Record<string, string> = {
'unknown': 'unknown',
'未知': 'unknown',
'other': 'other',
'其它': 'other',
'concept': 'concept',
'object': 'concept',
'type': 'concept',
'category': 'concept',
'model': 'concept',
'project': 'concept',
'condition': 'concept',
'rule': 'concept',
'regulation': 'concept',
'article': 'concept',
'law': 'concept',
'legalclause': 'concept',
'policy': 'concept',
'disease': 'concept',
'概念': 'concept',
'对象': 'concept',
'类别': 'concept',
'分类': 'concept',
'模型': 'concept',
'项目': 'concept',
'条件': 'concept',
'规则': 'concept',
'法律': 'concept',
'法律条款': 'concept',
'条文': 'concept',
'政策': 'policy',
'疾病': 'concept',
'method': 'method',
'process': 'method',
'方法': 'method',
'过程': 'method',
'artifact': 'artifact',
'technology': 'artifact',
'tech': 'artifact',
'product': 'artifact',
'equipment': 'artifact',
'device': 'artifact',
'stuff': 'artifact',
'component': 'artifact',
'material': 'artifact',
'chemical': 'artifact',
'drug': 'artifact',
'medicine': 'artifact',
'food': 'artifact',
'weapon': 'artifact',
'arms': 'artifact',
'人工制品': 'artifact',
'人造物品': 'artifact',
'技术': 'technology',
'科技': 'technology',
'产品': 'artifact',
'设备': 'artifact',
'装备': 'artifact',
'物品': 'artifact',
'材料': 'artifact',
'化学': 'artifact',
'药物': 'artifact',
'食品': 'artifact',
'武器': 'artifact',
'军火': 'artifact',
'naturalobject': 'naturalobject',
'natural': 'naturalobject',
'phenomena': 'naturalobject',
'substance': 'naturalobject',
'plant': 'naturalobject',
'自然对象': 'naturalobject',
'自然物体': 'naturalobject',
'自然现象': 'naturalobject',
'物质': 'naturalobject',
'物体': 'naturalobject',
'data': 'data',
'figure': 'data',
'value': 'data',
'数据': 'data',
'数字': 'data',
'数值': 'data',
'content': 'content',
'book': 'content',
'video': 'content',
'内容': 'content',
'作品': 'content',
'书籍': 'content',
'视频': 'content',
'organization': 'organization',
'org': 'organization',
'company': 'organization',
'组织': 'organization',
'公司': 'organization',
'机构': 'organization',
'组织机构': 'organization',
'event': 'event',
'事件': 'event',
'activity': 'event',
'活动': 'event',
'person': 'person',
'people': 'person',
'human': 'person',
'role': 'person',
'人物': 'person',
'人类': 'person',
'人': 'person',
'角色': 'person',
'creature': 'creature',
'animal': 'creature',
'beings': 'creature',
'being': 'creature',
'alien': 'creature',
'ghost': 'creature',
'动物': 'creature',
'生物': 'creature',
'神仙': 'creature',
'鬼怪': 'creature',
'妖怪': 'creature',
'location': 'location',
'geography': 'location',
'geo': 'location',
'place': 'location',
'address': 'location',
'地点': 'location',
'位置': 'location',
'地址': 'location',
'地理': 'location',
'地域': 'location',
};
// node type to color mapping
const NODE_TYPE_COLORS: Record<string, string> = {
'person': '#4169E1', // RoyalBlue
'creature': '#bd7ebe', // LightViolet
'organization': '#00cc00', // LightGreen
'location': '#cf6d17', // Carrot
'event': '#00bfa0', // Turquoise
'concept': '#e3493b', // GoogleRed
'method': '#b71c1c', // red
'content': '#0f558a', // NavyBlue
'data': '#0000ff', // Blue
'artifact': '#4421af', // DeepPurple
'naturalobject': '#b2e061', // YellowGreen
'other': '#f4d371', // Yellow
'unknown': '#b0b0b0', // Yellow
};
// Extended colors pool - Used for unknown node types
const EXTENDED_COLORS = [
'#84a3e1', // SkyBlue
'#5a2c6d', // DeepViolet
'#2F4F4F', // DarkSlateGray
'#003366', // DarkBlue
'#9b3a31', // DarkBrown
'#00CED1', // DarkTurquoise
'#b300b3', // Purple
'#0f705d', // Green
'#ff99cc', // Pale Pink
'#6ef7b3', // LightGreen
'#cd071e', // ChinaRed
];
import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor'
// Select color based on node type
const getNodeColorByType = (nodeType: string | undefined): string => {
const state = useGraphStore.getState()
const { color, map, updated } = resolveNodeColor(nodeType, state.typeColorMap)
const defaultColor = '#5D6D7E';
const normalizedType = nodeType ? nodeType.toLowerCase() : 'unknown';
const typeColorMap = useGraphStore.getState().typeColorMap;
// First try to find standard type
const standardType = TYPE_SYNONYMS[normalizedType];
// Check cache using standard type if available, otherwise use normalized type
const cacheKey = standardType || normalizedType;
if (typeColorMap.has(cacheKey)) {
return typeColorMap.get(cacheKey) || defaultColor;
if (updated) {
useGraphStore.setState({ typeColorMap: map })
}
if (standardType) {
const color = NODE_TYPE_COLORS[standardType];
// Store using standard type name as key
const newMap = new Map(typeColorMap);
newMap.set(standardType, color);
useGraphStore.setState({ typeColorMap: newMap });
return color;
}
// For unpredefind nodeTypes, use extended colors
// Find used extended colors
const usedExtendedColors = new Set(
Array.from(typeColorMap.entries())
.filter(([, color]) => !Object.values(NODE_TYPE_COLORS).includes(color))
.map(([, color]) => color)
);
// Find and use the first unused extended color
const unusedColor = EXTENDED_COLORS.find(color => !usedExtendedColors.has(color));
const newColor = unusedColor || defaultColor;
// Update color mapping - use normalized type for unknown types
const newMap = new Map(typeColorMap);
newMap.set(normalizedType, newColor);
useGraphStore.setState({ typeColorMap: newMap });
return newColor;
return color || DEFAULT_NODE_COLOR
};

View file

@ -2,6 +2,7 @@ import { create } from 'zustand'
import { createSelectors } from '@/lib/utils'
import { DirectedGraph } from 'graphology'
import MiniSearch from 'minisearch'
import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor'
export type RawNodeType = {
// for NetworkX: id is identical to properties['entity_id']
@ -246,7 +247,7 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
console.log('updateNodeAndSelect', nodeId, entityId, propertyName, newValue)
// For entity_id changes (node renaming) with NetworkX graph storage
// For entity_id changes (node renaming) with raw graph storage
if ((nodeId === entityId) && (propertyName === 'entity_id')) {
// Create new node with updated ID but same attributes
sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
@ -319,11 +320,21 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
// For non-NetworkX nodes or non-entity_id changes
const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
if (nodeIndex !== undefined) {
rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
const nodeRef = rawGraph.nodes[nodeIndex]
nodeRef.properties[propertyName] = newValue
if (propertyName === 'entity_id') {
rawGraph.nodes[nodeIndex].labels = [newValue]
nodeRef.labels = [newValue]
sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue)
}
if (propertyName === 'entity_type') {
const { color, map, updated } = resolveNodeColor(newValue, state.typeColorMap)
const resolvedColor = color || DEFAULT_NODE_COLOR
nodeRef.color = resolvedColor
sigmaGraph.setNodeAttribute(String(nodeId), 'color', resolvedColor)
if (updated) {
set({ typeColorMap: map })
}
}
}
// Trigger a re-render by incrementing the version counter

View file

@ -0,0 +1,228 @@
const DEFAULT_NODE_COLOR = '#5D6D7E'
const TYPE_SYNONYMS: Record<string, string> = {
unknown: 'unknown',
: 'unknown',
other: 'other',
: 'other',
concept: 'concept',
object: 'concept',
type: 'concept',
category: 'concept',
model: 'concept',
project: 'concept',
condition: 'concept',
rule: 'concept',
regulation: 'concept',
article: 'concept',
law: 'concept',
legalclause: 'concept',
policy: 'concept',
disease: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'concept',
: 'policy',
: 'concept',
method: 'method',
process: 'method',
: 'method',
: 'method',
artifact: 'artifact',
technology: 'artifact',
tech: 'artifact',
product: 'artifact',
equipment: 'artifact',
device: 'artifact',
stuff: 'artifact',
component: 'artifact',
material: 'artifact',
chemical: 'artifact',
drug: 'artifact',
medicine: 'artifact',
food: 'artifact',
weapon: 'artifact',
arms: 'artifact',
: 'artifact',
: 'artifact',
: 'technology',
: 'technology',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
: 'artifact',
naturalobject: 'naturalobject',
natural: 'naturalobject',
phenomena: 'naturalobject',
substance: 'naturalobject',
plant: 'naturalobject',
: 'naturalobject',
: 'naturalobject',
: 'naturalobject',
: 'naturalobject',
: 'naturalobject',
data: 'data',
figure: 'data',
value: 'data',
: 'data',
: 'data',
: 'data',
content: 'content',
book: 'content',
video: 'content',
: 'content',
: 'content',
: 'content',
: 'content',
organization: 'organization',
org: 'organization',
company: 'organization',
: 'organization',
: 'organization',
: 'organization',
: 'organization',
event: 'event',
: 'event',
activity: 'event',
: 'event',
person: 'person',
people: 'person',
human: 'person',
role: 'person',
: 'person',
: 'person',
: 'person',
: 'person',
creature: 'creature',
animal: 'creature',
beings: 'creature',
being: 'creature',
alien: 'creature',
ghost: 'creature',
: 'creature',
: 'creature',
: 'creature',
: 'creature',
: 'creature',
location: 'location',
geography: 'location',
geo: 'location',
place: 'location',
address: 'location',
: 'location',
: 'location',
: 'location',
: 'location',
: 'location'
}
const NODE_TYPE_COLORS: Record<string, string> = {
person: '#4169E1',
creature: '#bd7ebe',
organization: '#00cc00',
location: '#cf6d17',
event: '#00bfa0',
concept: '#e3493b',
method: '#b71c1c',
content: '#0f558a',
data: '#0000ff',
artifact: '#4421af',
naturalobject: '#b2e061',
other: '#f4d371',
unknown: '#b0b0b0'
}
const EXTENDED_COLORS = [
'#84a3e1',
'#5a2c6d',
'#2F4F4F',
'#003366',
'#9b3a31',
'#00CED1',
'#b300b3',
'#0f705d',
'#ff99cc',
'#6ef7b3',
'#cd071e'
]
const PREDEFINED_COLOR_SET = new Set(Object.values(NODE_TYPE_COLORS))
interface ResolveNodeColorResult {
color: string
map: Map<string, string>
updated: boolean
}
export const resolveNodeColor = (
nodeType: string | undefined,
currentMap: Map<string, string> | undefined
): ResolveNodeColorResult => {
const typeColorMap = currentMap ?? new Map<string, string>()
const normalizedType = nodeType ? nodeType.toLowerCase() : 'unknown'
const standardType = TYPE_SYNONYMS[normalizedType]
const cacheKey = standardType || normalizedType
if (typeColorMap.has(cacheKey)) {
return {
color: typeColorMap.get(cacheKey) || DEFAULT_NODE_COLOR,
map: typeColorMap,
updated: false
}
}
if (standardType) {
const color = NODE_TYPE_COLORS[standardType] || DEFAULT_NODE_COLOR
const newMap = new Map(typeColorMap)
newMap.set(standardType, color)
return {
color,
map: newMap,
updated: true
}
}
const usedExtendedColors = new Set(
Array.from(typeColorMap.values()).filter((color) => !PREDEFINED_COLOR_SET.has(color))
)
const unusedColor = EXTENDED_COLORS.find((color) => !usedExtendedColors.has(color))
const color = unusedColor || DEFAULT_NODE_COLOR
const newMap = new Map(typeColorMap)
newMap.set(normalizedType, color)
return {
color,
map: newMap,
updated: true
}
}
export { DEFAULT_NODE_COLOR }