@@ -6,6 +6,7 @@ import { toast } from 'sonner'
import { updateEntity , updateRelation , checkEntityNameExists } from '@/api/lightrag'
import { useGraphStore } from '@/stores/graph'
import { PencilIcon } from 'lucide-react'
import { tr } from '@faker-js/faker'
interface EditablePropertyRowProps {
name : string
@@ -26,7 +27,7 @@ interface EditablePropertyRowProps {
*/
const EditablePropertyRow = ( {
name ,
value ,
value : initialValue ,
onClick ,
tooltip ,
entityId ,
@@ -40,12 +41,18 @@ const EditablePropertyRow = ({
const [ isEditing , setIsEditing ] = useState ( false )
const [ editValue , setEditValue ] = useState ( '' )
const [ isSubmitting , setIsSubmitting ] = useState ( false )
const [ currentValue , setCurrentValue ] = useState ( initialValue )
const inputRef = useRef < HTMLInputElement > ( null )
// Update currentValue when initialValue changes
useEffect ( ( ) = > {
setCurrentValue ( initialValue )
} , [ initialValue ] )
// Initialize edit value when entering edit mode
useEffect ( ( ) = > {
if ( isEditing ) {
setEditValue ( String ( v alue) )
setEditValue ( String ( currentV alue) )
// Focus the input element when entering edit mode
setTimeout ( ( ) = > {
if ( inputRef . current ) {
@@ -54,7 +61,7 @@ const EditablePropertyRow = ({
}
} , 50 )
}
} , [ isEditing , v alue] )
} , [ isEditing , currentV alue] )
const getPropertyNameTranslation = ( propName : string ) = > {
const translationKey = ` graphPanel.propertiesView.node.propertyNames. ${ propName } `
@@ -91,10 +98,34 @@ const EditablePropertyRow = ({
if ( propertyName === 'entity_id' ) {
sigmaGraph . addNode ( newValue , { . . . nodeAttributes , label : newValue } )
interface EdgeToUpdate {
originalDynamicId : string ;
newEdgeId : string ;
edgeIndex : number ;
}
const edgesToUpdate : EdgeToUpdate [ ] = [ ] ;
sigmaGraph . forEachEdge ( String ( nodeId ) , ( edge , attributes , source , target ) = > {
const otherNode = source === String ( nodeId ) ? target : source
const isOutgoing = source === String ( nodeId )
sigmaGraph . addEdge ( isOutgoing ? newValue : otherNode , isOutgoing ? otherNode : newValue , attributes )
// 获取原始边的dynamicId, 以便后续更新edgeDynamicIdMap
const originalEdgeDynamicId = edge
const edgeIndexInRawGraph = rawGraph . edgeDynamicIdMap [ originalEdgeDynamicId ]
// 创建新边并获取新边的ID
const newEdgeId = sigmaGraph . addEdge ( isOutgoing ? newValue : otherNode , isOutgoing ? otherNode : newValue , attributes )
// 存储需要更新的边信息
if ( edgeIndexInRawGraph !== undefined ) {
edgesToUpdate . push ( {
originalDynamicId : originalEdgeDynamicId ,
newEdgeId : newEdgeId ,
edgeIndex : edgeIndexInRawGraph
} )
}
sigmaGraph . dropEdge ( edge )
} )
@@ -107,42 +138,112 @@ const EditablePropertyRow = ({
delete rawGraph . nodeIdMap [ String ( nodeId ) ]
rawGraph . nodeIdMap [ newValue ] = nodeIndex
}
} else {
const updatedAttributes = { . . . nodeAttributes }
if ( propertyName === 'description' ) {
updatedAttributes . description = newValue
// 更新边的引用关系
edgesToUpdate . forEach ( ( { originalDynamicId , newEdgeId , edgeIndex } ) = > {
// 更新边的source和target
if ( rawGraph . edges [ edgeIndex ] ) {
if ( rawGraph . edges [ edgeIndex ] . source === String ( nodeId ) ) {
rawGraph . edges [ edgeIndex ] . source = newValue
}
if ( rawGraph . edges [ edgeIndex ] . target === String ( nodeId ) ) {
rawGraph . edges [ edgeIndex ] . target = newValue
}
// 更新dynamicId映射
rawGraph . edges [ edgeIndex ] . dynamicId = newEdgeId
delete rawGraph . edgeDynamicIdMap [ originalDynamicId ]
rawGraph . edgeDynamicIdMap [ newEdgeId ] = edgeIndex
}
Object . entries ( updatedAttributes ) . forEach ( ( [ key , value ] ) = > {
sigmaGraph . setNodeAttribute ( String ( nodeId ) , key , value )
} )
useGraphStore . getState ( ) . setSelectedNode ( editValue )
} else {
// const updatedAttributes = { ...nodeAttributes }
// if (propertyName === 'description') {
// updatedAttributes.description = newValue
// }
// Object.entries(updatedAttributes).forEach(([key, value]) => {
// sigmaGraph.setNodeAttribute(String(nodeId), key, value)
// })
const nodeIndex = rawGraph . nodeIdMap [ String ( nodeId ) ]
if ( nodeIndex !== undefined ) {
rawGraph . nodes [ nodeIndex ] . properties [ propertyName ] = newValue
}
}
const selectedNode = useGraphStore . getState ( ) . selectedNode
if ( selectedNode === String ( nodeId ) ) {
useGraphStore . getState ( ) . setSelectedNode ( newValue )
}
const focusedNode = useGraphStore . getState ( ) . focusedNode
if ( focusedNode === String ( nodeId ) ) {
useGraphStore . getState ( ) . setFocusedNode ( newValue )
}
sigmaInstance . refresh ( )
} catch ( error ) {
console . error ( 'Error updating node in graph:' , error )
throw new Error ( 'Failed to update node in graph' )
}
}
const updateGraphEdge = async ( sourceId : string , targetId : string , propertyName : string , newValue : string ) = > {
const sigmaInstance = useGraphStore . getState ( ) . sigmaInstance
const sigmaGraph = useGraphStore . getState ( ) . sigmaGraph
const rawGraph = useGraphStore . getState ( ) . rawGraph
if ( ! sigmaInstance || ! sigmaGraph || ! rawGraph ) {
return
}
try {
const allEdges = sigmaGraph . edges ( )
let keyToUse = null
for ( const edge of allEdges ) {
const edgeSource = sigmaGraph . source ( edge )
const edgeTarget = sigmaGraph . target ( edge )
if ( ( edgeSource === sourceId && edgeTarget === targetId ) ||
( edgeSource === targetId && edgeTarget === sourceId ) ) {
keyToUse = edge
break
}
}
if ( keyToUse !== null ) {
if ( propertyName === 'keywords' ) {
sigmaGraph . setEdgeAttribute ( keyToUse , 'label' , newValue ) ;
} else {
sigmaGraph . setEdgeAttribute ( keyToUse , propertyName , newValue ) ;
}
if ( keyToUse && rawGraph . edgeDynamicIdMap [ keyToUse ] !== undefined ) {
const edgeIndex = rawGraph . edgeDynamicIdMap [ keyToUse ] ;
if ( rawGraph . edges [ edgeIndex ] ) {
rawGraph . edges [ edgeIndex ] . properties [ propertyName ] = newValue ;
} else {
console . warn ( ` Edge index ${ edgeIndex } found but edge data missing in rawGraph for dynamicId ${ entityId } ` ) ;
}
} else {
console . warn ( ` Could not find edge with dynamicId ${ entityId } in rawGraph.edgeDynamicIdMap to update properties. ` ) ;
if ( keyToUse !== null ) {
const edgeIndexByKey = rawGraph . edgeIdMap [ keyToUse ] ;
if ( edgeIndexByKey !== undefined && rawGraph . edges [ edgeIndexByKey ] ) {
rawGraph . edges [ edgeIndexByKey ] . properties [ propertyName ] = newValue ;
console . log ( ` Updated rawGraph edge using constructed key ${ keyToUse } ` ) ;
} else {
console . warn ( ` Could not find edge in rawGraph using key ${ keyToUse } either. ` ) ;
}
} else {
console . warn ( 'Cannot update edge properties: edge key is null' ) ;
}
}
} else {
console . warn ( ` Edge not found in sigmaGraph with key ${ keyToUse } ` ) ;
}
} catch ( error ) {
// Log the specific edge key that caused the error if possible
console . error ( ` Error updating edge ${ sourceId } -> ${ targetId } in graph: ` , error ) ;
throw new Error ( 'Failed to update edge in graph' )
}
}
const handleSave = async ( ) = > {
if ( isSubmitting ) return
if ( editValue === String ( v alue) ) {
if ( editValue === String ( currentV alue) ) {
setIsEditing ( false )
return
}
@@ -150,112 +251,71 @@ const EditablePropertyRow = ({
setIsSubmitting ( true )
try {
const updatedData : Record < string , any > = { }
if ( entityType === 'node' && entityId ) {
let updatedData = { [ name ] : editValue }
if ( name === 'entity_id' ) {
if ( editValue !== String ( value ) ) {
const exists = await checkEntityNameExists ( editValue )
if ( exists ) {
toast . error ( t ( 'graphPanel.propertiesView.errors.duplicateName' ) )
setIsSubmitting ( false )
return
}
updatedData = { 'entity_name' : editValue }
}
updatedData [ 'entity_name' ] = editValue
await updateEntity ( String ( value ) , updatedData , true )
await updateGraphNode ( String ( value ) , 'entity_id' , editValue )
} else {
updatedData [ name ] = editValue
await updateEntity ( entityId , updatedData )
if ( name === 'description' ) {
await updateEntity ( entityId , updatedData , true )
await updateGraphNode ( entityId , name , editValue )
}
}
toast . success ( t ( 'graphPanel.propertiesView.success.entityUpdated' ) )
} else if ( entityType === 'edge' && sourceId && targetId ) {
updatedData [ name ] = editValue
const updatedData = { [ name ] : editValue }
await updateRelation ( sourceId , targetId , updatedData )
await updateGraphEdge ( sourceId , targetId , name , editValue )
toast . success ( t ( 'graphPanel.propertiesView.success.relationUpdated' ) )
}
if ( onValueChang e) {
onValueChang e ( editValue )
}
useGraphStore . getState ( ) . setGraphDataFetchAttempted ( false )
useGraphStore . getState ( ) . setLabelsFetchAttempted ( false )
const currentNodeId = name === 'entity_id' ? editValue : ( entityId || '' )
useGraphStore . getState ( ) . setSelectedNode ( null )
useGraphStore . getState ( ) . setSelectedNode ( currentNodeId )
} catch ( error : any ) {
setIsEditing ( fals e)
setCurrentValu e( editValue )
} catch ( error ) {
console . error ( 'Error updating property:' , error )
let detailMessage = t ( 'graphPanel.propertiesView.errors.updateFailed' )
if ( error . response ? . data ? . detail ) {
detailMessage = error . response . data . detail
} else if ( error . response ? . data ? . message ) {
detailMessage = error . response . data . message
} else if ( error . message ) {
detailMessage = error . message
}
console . error ( 'Update failed:' , {
entityType ,
entityId ,
propertyName : name ,
newValue : editValue ,
error : error.response?.data || error . message
} )
toast . error ( detailMessage , {
description : t ( 'graphPanel.propertiesView.errors.tryAgainLater' )
} )
toast . error ( t ( 'graphPanel.propertiesView.errors.updateFailed' ) )
} finally {
setIsSubmitting ( false )
setIsEditing ( false )
}
}
// Determine if this property should be editabl e
// Currently only 'description' and 'entity_id' fields are editable
const isEditableField = isEditable && ( name === 'description' || name === 'entity_id' )
// Always render the property name label and edit icon, regardless of edit stat e
return (
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-1 text-primary/60 tracking-wide whitespace-nowrap" >
{ getPropertyNameTranslation ( name ) }
{ isEditableField && (
< div className = "flex items-center gap-1" onDoubleClick = { handleDoubleClick } >
< span className = "text-primary/60 tracking-wide whitespace-nowrap" > { getPropertyNameTranslation ( name ) } < / span >
< div className = "group relative" >
< PencilIcon className = "w-3 h-3 opacity-50 hover:opacity-100" / >
< div className = "absolute left-5 transform -translate-y-full -top-2 bg-primary/90 text-white text-xs px-3 py-1.5 rounded shadow-lg border border-primary/20 opacity-0 group-hover:opacit y-1 00 transition-opacity whitespace-nowrap z-[100]" >
< PencilIcon
className = "h-3 w-3 text-gray-500 hover:text-gra y-7 00 cursor-pointer"
onClick = { ( ) = > setIsEditing ( true ) }
/ >
< div className = "absolute left-5 transform -translate-y-full -top-2 bg-primary/90 text-white text-xs px-3 py-1.5 rounded shadow-lg border border-primary/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap z-[100]" >
{ t ( 'graphPanel.propertiesView.doubleClickToEdit' ) }
< / div >
< / div >
) }
< / div > :
{ isEditing ? (
< div className = "flex-1" >
// Render input field when editing
< Input
ref = { inputRef }
className = "h-7 text-xs w-full "
type = "text "
value = { editValue }
onChange = { ( e ) = > setEditValue ( e . target . value ) }
onBlur = { handleSave }
onKeyDown = { handleKeyDown }
onBlur = { handleSave }
className = "h-6 text-xs"
disabled = { isSubmitting }
/ >
< / div >
) : (
< div
className = { ` flex-1 overflow-hidden ${ isEditableField ? 'cursor-text' : '' } ` }
onDoubleClick = { isEditableField ? handleDoubleClick : undefined }
>
// Render text component when not editing
< div className = "flex items-center gap-1" >
< Text
className = "hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis block w-full "
className = "hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
tooltipClassName = "max-w-80"
text = { String ( v alue) }
tooltip = { tooltip || ( typeof v alue === 'string' ? v alue : JSON.stringify ( v alue, null , 2 ) ) }
text = { currentV alue}
tooltip = { tooltip || ( typeof currentV alue === 'string' ? currentV alue : JSON.stringify ( currentV alue, null , 2 ) ) }
side = "left"
onClick = { onClick }
/ >