From 7e3e6857638d701fbf01739edf2e39cc1c6a0f15 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sat, 12 Apr 2025 00:48:19 +0800 Subject: [PATCH 01/20] feat(graph): Add editing function for entity and relationship attributes --- lightrag/api/routers/graph_routes.py | 62 ++++- lightrag_webui/src/api/lightrag.ts | 55 ++++ .../components/graph/EditablePropertyRow.tsx | 245 ++++++++++++++++++ .../src/components/graph/PropertiesView.tsx | 57 +++- lightrag_webui/src/lib/constants.ts | 2 +- lightrag_webui/src/stores/graph.ts | 8 + 6 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 lightrag_webui/src/components/graph/EditablePropertyRow.tsx diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index 381df90b..97a397d4 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -2,14 +2,21 @@ This module contains all graph-related routes for the LightRAG API. """ -from typing import Optional -from fastapi import APIRouter, Depends, Query +from typing import Optional, Dict, Any +from fastapi import APIRouter, Depends, Query, HTTPException +from pydantic import BaseModel from ..utils_api import get_combined_auth_dependency router = APIRouter(tags=["graph"]) +class EntityUpdateRequest(BaseModel): + entity_name: str + updated_data: Dict[str, Any] + allow_rename: bool = False + + def create_graph_routes(rag, api_key: Optional[str] = None): combined_auth = get_combined_auth_dependency(api_key) @@ -49,4 +56,55 @@ def create_graph_routes(rag, api_key: Optional[str] = None): max_nodes=max_nodes, ) + @router.get("/graph/entity/exists", dependencies=[Depends(combined_auth)]) + async def check_entity_exists( + name: str = Query(..., description="Entity name to check"), + ): + """ + Check if an entity with the given name exists in the knowledge graph + + Args: + name (str): Name of the entity to check + + Returns: + Dict[str, bool]: Dictionary with 'exists' key indicating if entity exists + """ + try: + exists = await rag.chunk_entity_relation_graph.has_node(name) + return {"exists": exists} + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error checking entity existence: {str(e)}" + ) + + @router.post("/graph/entity/edit", dependencies=[Depends(combined_auth)]) + async def update_entity(request: EntityUpdateRequest): + """ + Update an entity's properties in the knowledge graph + + Args: + request (EntityUpdateRequest): Request containing entity name, updated data, and rename flag + + Returns: + Dict: Updated entity information + """ + try: + print(request.entity_name, request.updated_data, request.allow_rename) + result = await rag.aedit_entity( + entity_name=request.entity_name, + updated_data=request.updated_data, + allow_rename=request.allow_rename, + ) + return { + "status": "success", + "message": "Entity updated successfully", + "data": result, + } + except ValueError as ve: + raise HTTPException(status_code=400, detail=str(ve)) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error updating entity: {str(e)}" + ) + return router diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index fbe00d23..63f4e2f6 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -506,3 +506,58 @@ export const loginToServer = async (username: string, password: string): Promise return response.data; } + +/** + * Updates an entity's properties in the knowledge graph + * @param entityName The name of the entity to update + * @param updatedData Dictionary containing updated attributes + * @param allowRename Whether to allow renaming the entity (default: false) + * @returns Promise with the updated entity information + */ +export const updateEntity = async ( + entityName: string, + updatedData: Record, + allowRename: boolean = false +): Promise => { + const response = await axiosInstance.post('/graph/entity/edit', { + entity_name: entityName, + updated_data: updatedData, + allow_rename: allowRename + }) + return response.data +} + +/** + * Updates a relation's properties in the knowledge graph + * @param sourceEntity The source entity name + * @param targetEntity The target entity name + * @param updatedData Dictionary containing updated attributes + * @returns Promise with the updated relation information + */ +export const updateRelation = async ( + sourceEntity: string, + targetEntity: string, + updatedData: Record +): Promise => { + const response = await axiosInstance.post('/graph/relation/edit', { + source_entity: sourceEntity, + target_entity: targetEntity, + updated_data: updatedData + }) + return response.data +} + +/** + * Checks if an entity name already exists in the knowledge graph + * @param entityName The entity name to check + * @returns Promise with boolean indicating if the entity exists + */ +export const checkEntityNameExists = async (entityName: string): Promise => { + try { + const response = await axiosInstance.get(`/graph/entity/exists?name=${encodeURIComponent(entityName)}`) + return response.data.exists + } catch (error) { + console.error('Error checking entity name:', error) + return false + } +} diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx new file mode 100644 index 00000000..dfd94e49 --- /dev/null +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -0,0 +1,245 @@ +import { useState, useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import Text from '@/components/ui/Text' +import Input from '@/components/ui/Input' +import { toast } from 'sonner' +import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' +import { useGraphStore } from '@/stores/graph' +import { useSettingsStore } from '@/stores/settings' + +interface EditablePropertyRowProps { + name: string + value: any + onClick?: () => void + tooltip?: string + entityId?: string + entityType?: 'node' | 'edge' + sourceId?: string + targetId?: string + onValueChange?: (newValue: any) => void + isEditable?: boolean +} + +/** + * EditablePropertyRow component that supports double-click to edit property values + * Specifically designed for editing 'description' and entity name fields + */ +const EditablePropertyRow = ({ + name, + value, + onClick, + tooltip, + entityId, + entityType, + sourceId, + targetId, + onValueChange, + isEditable = false +}: EditablePropertyRowProps) => { + const { t } = useTranslation() + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const inputRef = useRef(null) + + // Initialize edit value when entering edit mode + useEffect(() => { + if (isEditing) { + setEditValue(String(value)) + // Focus the input element when entering edit mode + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.select() + } + }, 50) + } + }, [isEditing, value]) + + const getPropertyNameTranslation = (propName: string) => { + const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}` + const translation = t(translationKey) + return translation === translationKey ? propName : translation + } + + const handleDoubleClick = () => { + if (isEditable && !isEditing) { + setIsEditing(true) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSave() + } else if (e.key === 'Escape') { + setIsEditing(false) + } + } + + const handleSave = async () => { + if (isSubmitting) return + + // Don't save if value hasn't changed + if (editValue === String(value)) { + setIsEditing(false) + return + } + + setIsSubmitting(true) + + try { + // Special handling for entity_id (name) field to check for duplicates + if (name === 'entity_id' && entityType === 'node') { + // Ensure we are not checking the original name against itself if it's protected + if (editValue !== String(value)) { + const exists = await checkEntityNameExists(editValue); + if (exists) { + toast.error(t('graphPanel.propertiesView.errors.duplicateName')); + setIsSubmitting(false); + return; + } + } + } + + // Update the entity or relation in the database + if (entityType === 'node' && entityId) { + // For nodes, we need to determine if we're updating the name or description + const updatedData: Record = {} + + if (name === 'entity_id') { + // For entity name updates + updatedData['entity_name'] = editValue + await updateEntity(String(value), updatedData, true) // Pass original name (value) as identifier + + // Update node label in the graph directly instead of reloading the entire graph + const sigmaInstance = useGraphStore.getState().sigmaInstance + const sigmaGraph = useGraphStore.getState().sigmaGraph + const rawGraph = useGraphStore.getState().rawGraph + + if (sigmaInstance && sigmaGraph && rawGraph) { + // Update the node in sigma graph + if (sigmaGraph.hasNode(String(value))) { + // Update the node label in the sigma graph + sigmaGraph.setNodeAttribute(String(value), 'label', editValue) + + // Also update the node in the raw graph + const nodeIndex = rawGraph.nodeIdMap[String(value)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].id = editValue + // Update the node ID map + delete rawGraph.nodeIdMap[String(value)] + rawGraph.nodeIdMap[editValue] = nodeIndex + } + + // Refresh the sigma instance to reflect changes + sigmaInstance.refresh() + + // Update selected node ID if it was the edited node + const selectedNode = useGraphStore.getState().selectedNode + if (selectedNode === String(value)) { + useGraphStore.getState().setSelectedNode(editValue) + } + } + } else { + // Fallback to full graph reload if direct update is not possible + useGraphStore.getState().setGraphDataFetchAttempted(false) + useGraphStore.getState().setLabelsFetchAttempted(false) + + // Get current label to trigger reload + const currentLabel = useSettingsStore.getState().queryLabel + if (currentLabel) { + // Trigger data reload by temporarily clearing and resetting the label + useSettingsStore.getState().setQueryLabel('') + setTimeout(() => { + useSettingsStore.getState().setQueryLabel(currentLabel) + }, 0) + } + } + } else if (name === 'description') { + // For description updates + updatedData['description'] = editValue + await updateEntity(entityId, updatedData) // Pass entityId as identifier + } else { + // For other property updates + updatedData[name] = editValue + await updateEntity(entityId, updatedData) // Pass entityId as identifier + } + + toast.success(t('graphPanel.propertiesView.success.entityUpdated')) + } else if (entityType === 'edge' && sourceId && targetId) { + // For edges, update the relation + const updatedData: Record = {} + updatedData[name] = editValue + await updateRelation(sourceId, targetId, updatedData) + toast.success(t('graphPanel.propertiesView.success.relationUpdated')) + } + + // Notify parent component about the value change + if (onValueChange) { + onValueChange(editValue) + } + } catch (error: any) { // Keep type as any to access potential response properties + console.error('Error updating property:', error); + + // Attempt to extract a more specific error message + let detailMessage = t('graphPanel.propertiesView.errors.updateFailed'); // Default message + if (error.response?.data?.detail) { + // Use the detailed message from the backend response if available + detailMessage = error.response.data.detail; + } else if (error.message) { + // Use the error object's message if no backend detail + detailMessage = error.message; + } + + toast.error(detailMessage); // Show the determined error message + + } finally { + setIsSubmitting(false) + setIsEditing(false) + } + } + + // Determine if this property should be editable + // Currently only 'description' and 'entity_id' fields are editable + const isEditableField = isEditable && (name === 'description' || name === 'entity_id') + + return ( +
+ + {getPropertyNameTranslation(name)} + : + {isEditing ? ( +
+ setEditValue(e.target.value)} + onBlur={handleSave} + onKeyDown={handleKeyDown} + disabled={isSubmitting} + /> +
+ ) : ( + // Wrap Text component in a div to handle onDoubleClick +
+ +
+ )} +
+ ) +} + +export default EditablePropertyRow diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index 3aa248de..c2cda263 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -5,6 +5,7 @@ import Button from '@/components/ui/Button' import useLightragGraph from '@/hooks/useLightragGraph' import { useTranslation } from 'react-i18next' import { GitBranchPlus, Scissors } from 'lucide-react' +import EditablePropertyRow from './EditablePropertyRow' /** * Component that view properties of elements in graph. @@ -169,12 +170,22 @@ const PropertyRow = ({ name, value, onClick, - tooltip + tooltip, + entityId, + entityType, + sourceId, + targetId, + isEditable = false }: { name: string value: any onClick?: () => void tooltip?: string + entityId?: string + entityType?: 'node' | 'edge' + sourceId?: string + targetId?: string + isEditable?: boolean }) => { const { t } = useTranslation() @@ -184,8 +195,24 @@ const PropertyRow = ({ return translation === translationKey ? name : translation } - // Since Text component uses a label internally, we'll use a span here instead of a label - // to avoid nesting labels which is not recommended for accessibility + // Use EditablePropertyRow for editable fields (description and entity_id) + if (isEditable && (name === 'description' || name === 'entity_id')) { + return ( + + ) + } + + // For non-editable fields, use the regular Text component return (
{getPropertyNameTranslation(name)}: @@ -253,7 +280,16 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => { {Object.keys(node.properties) .sort() .map((name) => { - return + return ( + + ) })}
{node.relationships.length > 0 && ( @@ -309,7 +345,18 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => { {Object.keys(edge.properties) .sort() .map((name) => { - return + return ( + + ) })} diff --git a/lightrag_webui/src/lib/constants.ts b/lightrag_webui/src/lib/constants.ts index 048ae8f7..87db8cea 100644 --- a/lightrag_webui/src/lib/constants.ts +++ b/lightrag_webui/src/lib/constants.ts @@ -1,6 +1,6 @@ import { ButtonVariantType } from '@/components/ui/Button' -export const backendBaseUrl = '' +export const backendBaseUrl = 'http://localhost:9621' export const webuiPrefix = '/webui/' export const controlButtonVariant: ButtonVariantType = 'ghost' diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index c50cff41..84000170 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -116,6 +116,10 @@ interface GraphState { // Node operation state nodeToExpand: string | null nodeToPrune: string | null + + // Version counter to trigger data refresh + graphDataVersion: number + incrementGraphDataVersion: () => void } const useGraphStoreBase = create()((set) => ({ @@ -219,6 +223,10 @@ const useGraphStoreBase = create()((set) => ({ triggerNodeExpand: (nodeId: string | null) => set({ nodeToExpand: nodeId }), triggerNodePrune: (nodeId: string | null) => set({ nodeToPrune: nodeId }), + // Version counter implementation + graphDataVersion: 0, + incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })), + })) const useGraphStore = createSelectors(useGraphStoreBase) From ea43f3537e0f6d7feb6fb63d54991c7c777cec24 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sat, 12 Apr 2025 10:36:05 +0800 Subject: [PATCH 02/20] fix(graph): Fixed the issue of incorrect handling of edges and nodes during node ID updates --- .../components/graph/EditablePropertyRow.tsx | 125 ++++++++++++------ .../src/components/graph/GraphControl.tsx | 12 +- lightrag_webui/src/locales/ar.json | 26 ++++ lightrag_webui/src/locales/en.json | 9 ++ lightrag_webui/src/locales/fr.json | 26 ++++ lightrag_webui/src/locales/zh.json | 9 ++ 6 files changed, 164 insertions(+), 43 deletions(-) diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index dfd94e49..837f6514 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -119,40 +119,62 @@ const EditablePropertyRow = ({ if (sigmaInstance && sigmaGraph && rawGraph) { // Update the node in sigma graph if (sigmaGraph.hasNode(String(value))) { - // Update the node label in the sigma graph - sigmaGraph.setNodeAttribute(String(value), 'label', editValue) + try { + // Create a new node with the updated ID + const oldNodeAttributes = sigmaGraph.getNodeAttributes(String(value)) - // Also update the node in the raw graph - const nodeIndex = rawGraph.nodeIdMap[String(value)] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].id = editValue - // Update the node ID map - delete rawGraph.nodeIdMap[String(value)] - rawGraph.nodeIdMap[editValue] = nodeIndex + // Add a new node with the new ID but keep all other attributes + sigmaGraph.addNode(editValue, { + ...oldNodeAttributes, + label: editValue + }) + + // Copy all edges from the old node to the new node + sigmaGraph.forEachEdge(String(value), (edge, attributes, source, target) => { + const otherNode = source === String(value) ? target : source + const isOutgoing = source === String(value) + + // Create a new edge with the same attributes but connected to the new node ID + if (isOutgoing) { + sigmaGraph.addEdge(editValue, otherNode, attributes) + } else { + sigmaGraph.addEdge(otherNode, editValue, attributes) + } + + // Remove the old edge + sigmaGraph.dropEdge(edge) + }) + + // Remove the old node after all edges have been transferred + sigmaGraph.dropNode(String(value)) + + // Also update the node in the raw graph + const nodeIndex = rawGraph.nodeIdMap[String(value)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].id = editValue + // Update the node ID map + delete rawGraph.nodeIdMap[String(value)] + rawGraph.nodeIdMap[editValue] = nodeIndex + } + + // Refresh the sigma instance to reflect changes + sigmaInstance.refresh() + + // Update selected node ID if it was the edited node + const selectedNode = useGraphStore.getState().selectedNode + if (selectedNode === String(value)) { + useGraphStore.getState().setSelectedNode(editValue) + } + + // Update focused node ID if it was the edited node + const focusedNode = useGraphStore.getState().focusedNode + if (focusedNode === String(value)) { + useGraphStore.getState().setFocusedNode(editValue) + } + } catch (error) { + console.error('Error updating node ID in graph:', error) + throw new Error('Failed to update node ID in graph') } - - // Refresh the sigma instance to reflect changes - sigmaInstance.refresh() - - // Update selected node ID if it was the edited node - const selectedNode = useGraphStore.getState().selectedNode - if (selectedNode === String(value)) { - useGraphStore.getState().setSelectedNode(editValue) - } - } - } else { - // Fallback to full graph reload if direct update is not possible - useGraphStore.getState().setGraphDataFetchAttempted(false) - useGraphStore.getState().setLabelsFetchAttempted(false) - - // Get current label to trigger reload - const currentLabel = useSettingsStore.getState().queryLabel - if (currentLabel) { - // Trigger data reload by temporarily clearing and resetting the label - useSettingsStore.getState().setQueryLabel('') - setTimeout(() => { - useSettingsStore.getState().setQueryLabel(currentLabel) - }, 0) } } } else if (name === 'description') { @@ -178,22 +200,45 @@ const EditablePropertyRow = ({ if (onValueChange) { onValueChange(editValue) } - } catch (error: any) { // Keep type as any to access potential response properties + } catch (error: any) { console.error('Error updating property:', error); - // Attempt to extract a more specific error message - let detailMessage = t('graphPanel.propertiesView.errors.updateFailed'); // Default message + // 尝试提取更具体的错误信息 + let detailMessage = t('graphPanel.propertiesView.errors.updateFailed'); + if (error.response?.data?.detail) { - // Use the detailed message from the backend response if available - detailMessage = error.response.data.detail; + detailMessage = error.response.data.detail; + } else if (error.response?.data?.message) { + detailMessage = error.response.data.message; } else if (error.message) { - // Use the error object's message if no backend detail - detailMessage = error.message; + detailMessage = error.message; } - toast.error(detailMessage); // Show the determined 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') + }); } finally { + // Update the value immediately in the UI + if (onValueChange) { + onValueChange(editValue); + } + // Trigger graph data refresh + useGraphStore.getState().setGraphDataFetchAttempted(false); + useGraphStore.getState().setLabelsFetchAttempted(false); + // Re-select the node to refresh properties panel + const currentNodeId = name === 'entity_id' ? editValue : (entityId || ''); + useGraphStore.getState().setSelectedNode(null); + useGraphStore.getState().setSelectedNode(currentNodeId); setIsSubmitting(false) setIsEditing(false) } diff --git a/lightrag_webui/src/components/graph/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx index aca8a9c4..8211178a 100644 --- a/lightrag_webui/src/components/graph/GraphControl.tsx +++ b/lightrag_webui/src/components/graph/GraphControl.tsx @@ -99,7 +99,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) const events: Record = { enterNode: (event: NodeEvent) => { if (!isButtonPressed(event.event.original)) { - setFocusedNode(event.node) + const graph = sigma.getGraph() + if (graph.hasNode(event.node)) { + setFocusedNode(event.node) + } } }, leaveNode: (event: NodeEvent) => { @@ -108,8 +111,11 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) } }, clickNode: (event: NodeEvent) => { - setSelectedNode(event.node) - setSelectedEdge(null) + const graph = sigma.getGraph() + if (graph.hasNode(event.node)) { + setSelectedNode(event.node) + setSelectedEdge(null) + } }, clickStage: () => clearSelection() } diff --git a/lightrag_webui/src/locales/ar.json b/lightrag_webui/src/locales/ar.json index 36f31e92..9b227d47 100644 --- a/lightrag_webui/src/locales/ar.json +++ b/lightrag_webui/src/locales/ar.json @@ -35,6 +35,32 @@ "common": { "cancel": "إلغاء" }, + "graphPanel": { + "propertiesView": { + "errors": { + "duplicateName": "اسم العقدة موجود بالفعل", + "updateFailed": "فشل تحديث العقدة", + "tryAgainLater": "يرجى المحاولة مرة أخرى لاحقاً" + }, + "success": { + "entityUpdated": "تم تحديث العقدة بنجاح", + "relationUpdated": "تم تحديث العلاقة بنجاح" + }, + "node": { + "title": "عقدة", + "id": "المعرف", + "labels": "التسميات", + "degree": "الدرجة", + "properties": "الخصائص", + "relationships": "العلاقات (ضمن الرسم البياني الفرعي)", + "expandNode": "توسيع العقدة", + "pruneNode": "تقليم العقدة", + "deleteAllNodesError": "رفض حذف جميع العقد في الرسم البياني", + "nodesRemoved": "تم حذف {{count}} عقدة، بما في ذلك العقد اليتيمة", + "noNewNodes": "لم يتم العثور على عقد قابلة للتوسيع" + } + } + }, "documentPanel": { "clearDocuments": { "button": "مسح", diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index 13b6e5a5..a7119cbd 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -235,6 +235,15 @@ "vectorStorage": "Vector Storage" }, "propertiesView": { + "errors": { + "duplicateName": "Node name already exists", + "updateFailed": "Failed to update node", + "tryAgainLater": "Please try again later" + }, + "success": { + "entityUpdated": "Node updated successfully", + "relationUpdated": "Relation updated successfully" + }, "node": { "title": "Node", "id": "ID", diff --git a/lightrag_webui/src/locales/fr.json b/lightrag_webui/src/locales/fr.json index dbba9480..f4894d86 100644 --- a/lightrag_webui/src/locales/fr.json +++ b/lightrag_webui/src/locales/fr.json @@ -35,6 +35,32 @@ "common": { "cancel": "Annuler" }, + "graphPanel": { + "propertiesView": { + "errors": { + "duplicateName": "Le nom du nœud existe déjà", + "updateFailed": "Échec de la mise à jour du nœud", + "tryAgainLater": "Veuillez réessayer plus tard" + }, + "success": { + "entityUpdated": "Nœud mis à jour avec succès", + "relationUpdated": "Relation mise à jour avec succès" + }, + "node": { + "title": "Nœud", + "id": "ID", + "labels": "Étiquettes", + "degree": "Degré", + "properties": "Propriétés", + "relationships": "Relations(dans le sous-graphe)", + "expandNode": "Développer le nœud", + "pruneNode": "Élaguer le nœud", + "deleteAllNodesError": "Refus de supprimer tous les nœuds du graphe", + "nodesRemoved": "{{count}} nœuds supprimés, y compris les nœuds orphelins", + "noNewNodes": "Aucun nœud extensible trouvé" + } + } + }, "documentPanel": { "clearDocuments": { "button": "Effacer", diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index df4c33c3..beeb56d4 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -236,6 +236,15 @@ "vectorStorage": "向量存储" }, "propertiesView": { + "errors": { + "duplicateName": "节点名称已存在", + "updateFailed": "更新节点失败", + "tryAgainLater": "请稍后重试" + }, + "success": { + "entityUpdated": "节点更新成功", + "relationUpdated": "关系更新成功" + }, "node": { "title": "节点", "id": "ID", From 58eeacda20b62a0bd3b842f02c7d12cccaaf39e3 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sat, 12 Apr 2025 13:17:09 +0800 Subject: [PATCH 03/20] refactor(graph): Refactoring node attribute update logic to improve code maintainability --- .../components/graph/EditablePropertyRow.tsx | 208 ++++++++---------- 1 file changed, 90 insertions(+), 118 deletions(-) diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index 837f6514..b6dc2588 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -5,7 +5,6 @@ import Input from '@/components/ui/Input' import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' -import { useSettingsStore } from '@/stores/settings' interface EditablePropertyRowProps { name: string @@ -76,10 +75,72 @@ const EditablePropertyRow = ({ } } + const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => { + const sigmaInstance = useGraphStore.getState().sigmaInstance + const sigmaGraph = useGraphStore.getState().sigmaGraph + const rawGraph = useGraphStore.getState().rawGraph + + if (!sigmaInstance || !sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) { + return + } + + try { + const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId)) + + if (propertyName === 'entity_id') { + sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) + + 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) + sigmaGraph.dropEdge(edge) + }) + + sigmaGraph.dropNode(String(nodeId)) + + const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].id = newValue + rawGraph.nodes[nodeIndex].properties.entity_id = newValue + delete rawGraph.nodeIdMap[String(nodeId)] + rawGraph.nodeIdMap[newValue] = nodeIndex + } + } 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 handleSave = async () => { if (isSubmitting) return - // Don't save if value hasn't changed if (editValue === String(value)) { setIsEditing(false) return @@ -88,157 +149,68 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { - // Special handling for entity_id (name) field to check for duplicates - if (name === 'entity_id' && entityType === 'node') { - // Ensure we are not checking the original name against itself if it's protected - if (editValue !== String(value)) { - const exists = await checkEntityNameExists(editValue); - if (exists) { - toast.error(t('graphPanel.propertiesView.errors.duplicateName')); - setIsSubmitting(false); - return; - } - } - } + const updatedData: Record = {} - // Update the entity or relation in the database if (entityType === 'node' && entityId) { - // For nodes, we need to determine if we're updating the name or description - const updatedData: Record = {} - if (name === 'entity_id') { - // For entity name updates - updatedData['entity_name'] = editValue - await updateEntity(String(value), updatedData, true) // Pass original name (value) as identifier - - // Update node label in the graph directly instead of reloading the entire graph - const sigmaInstance = useGraphStore.getState().sigmaInstance - const sigmaGraph = useGraphStore.getState().sigmaGraph - const rawGraph = useGraphStore.getState().rawGraph - - if (sigmaInstance && sigmaGraph && rawGraph) { - // Update the node in sigma graph - if (sigmaGraph.hasNode(String(value))) { - try { - // Create a new node with the updated ID - const oldNodeAttributes = sigmaGraph.getNodeAttributes(String(value)) - - // Add a new node with the new ID but keep all other attributes - sigmaGraph.addNode(editValue, { - ...oldNodeAttributes, - label: editValue - }) - - // Copy all edges from the old node to the new node - sigmaGraph.forEachEdge(String(value), (edge, attributes, source, target) => { - const otherNode = source === String(value) ? target : source - const isOutgoing = source === String(value) - - // Create a new edge with the same attributes but connected to the new node ID - if (isOutgoing) { - sigmaGraph.addEdge(editValue, otherNode, attributes) - } else { - sigmaGraph.addEdge(otherNode, editValue, attributes) - } - - // Remove the old edge - sigmaGraph.dropEdge(edge) - }) - - // Remove the old node after all edges have been transferred - sigmaGraph.dropNode(String(value)) - - // Also update the node in the raw graph - const nodeIndex = rawGraph.nodeIdMap[String(value)] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].id = editValue - // Update the node ID map - delete rawGraph.nodeIdMap[String(value)] - rawGraph.nodeIdMap[editValue] = nodeIndex - } - - // Refresh the sigma instance to reflect changes - sigmaInstance.refresh() - - // Update selected node ID if it was the edited node - const selectedNode = useGraphStore.getState().selectedNode - if (selectedNode === String(value)) { - useGraphStore.getState().setSelectedNode(editValue) - } - - // Update focused node ID if it was the edited node - const focusedNode = useGraphStore.getState().focusedNode - if (focusedNode === String(value)) { - useGraphStore.getState().setFocusedNode(editValue) - } - } catch (error) { - console.error('Error updating node ID in graph:', error) - throw new Error('Failed to update node ID in graph') - } + if (editValue !== String(value)) { + const exists = await checkEntityNameExists(editValue) + if (exists) { + toast.error(t('graphPanel.propertiesView.errors.duplicateName')) + return } } - } else if (name === 'description') { - // For description updates - updatedData['description'] = editValue - await updateEntity(entityId, updatedData) // Pass entityId as identifier + updatedData['entity_name'] = editValue + await updateEntity(String(value), updatedData, true) + await updateGraphNode(String(value), 'entity_id', editValue) } else { - // For other property updates updatedData[name] = editValue - await updateEntity(entityId, updatedData) // Pass entityId as identifier + await updateEntity(entityId, updatedData) + if (name === 'description') { + await updateGraphNode(entityId, name, editValue) + } } - toast.success(t('graphPanel.propertiesView.success.entityUpdated')) } else if (entityType === 'edge' && sourceId && targetId) { - // For edges, update the relation - const updatedData: Record = {} updatedData[name] = editValue await updateRelation(sourceId, targetId, updatedData) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } - // Notify parent component about the value change if (onValueChange) { onValueChange(editValue) } - } catch (error: any) { - console.error('Error updating property:', error); - // 尝试提取更具体的错误信息 - let detailMessage = t('graphPanel.propertiesView.errors.updateFailed'); + 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) { + console.error('Error updating property:', error) + let detailMessage = t('graphPanel.propertiesView.errors.updateFailed') if (error.response?.data?.detail) { - detailMessage = error.response.data.detail; + detailMessage = error.response.data.detail } else if (error.response?.data?.message) { - detailMessage = error.response.data.message; + detailMessage = error.response.data.message } else if (error.message) { - detailMessage = 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') - }); - + }) } finally { - // Update the value immediately in the UI - if (onValueChange) { - onValueChange(editValue); - } - // Trigger graph data refresh - useGraphStore.getState().setGraphDataFetchAttempted(false); - useGraphStore.getState().setLabelsFetchAttempted(false); - // Re-select the node to refresh properties panel - const currentNodeId = name === 'entity_id' ? editValue : (entityId || ''); - useGraphStore.getState().setSelectedNode(null); - useGraphStore.getState().setSelectedNode(currentNodeId); setIsSubmitting(false) setIsEditing(false) } From 272b10197436b05b89c5bba91b6a5d087394c659 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sat, 12 Apr 2025 14:33:40 +0800 Subject: [PATCH 04/20] feat: Add double-click editing prompt text and optimize editable attribute line style --- .../components/graph/EditablePropertyRow.tsx | 20 ++++++---- lightrag_webui/src/locales/ar.json | 36 +++++------------- lightrag_webui/src/locales/en.json | 1 + lightrag_webui/src/locales/fr.json | 37 ++++++------------- lightrag_webui/src/locales/zh.json | 1 + 5 files changed, 36 insertions(+), 59 deletions(-) diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index b6dc2588..bf583a0e 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -5,6 +5,7 @@ import Input from '@/components/ui/Input' import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' +import { PencilIcon } from 'lucide-react' interface EditablePropertyRowProps { name: string @@ -222,9 +223,17 @@ const EditablePropertyRow = ({ return (
- +
{getPropertyNameTranslation(name)} - : + {isEditableField && ( +
+ +
+ {t('graphPanel.propertiesView.doubleClickToEdit')} +
+
+ )} +
: {isEditing ? (
) : ( - // Wrap Text component in a div to handle onDoubleClick
)} diff --git a/lightrag_webui/src/locales/ar.json b/lightrag_webui/src/locales/ar.json index 9b227d47..315bfe48 100644 --- a/lightrag_webui/src/locales/ar.json +++ b/lightrag_webui/src/locales/ar.json @@ -35,32 +35,6 @@ "common": { "cancel": "إلغاء" }, - "graphPanel": { - "propertiesView": { - "errors": { - "duplicateName": "اسم العقدة موجود بالفعل", - "updateFailed": "فشل تحديث العقدة", - "tryAgainLater": "يرجى المحاولة مرة أخرى لاحقاً" - }, - "success": { - "entityUpdated": "تم تحديث العقدة بنجاح", - "relationUpdated": "تم تحديث العلاقة بنجاح" - }, - "node": { - "title": "عقدة", - "id": "المعرف", - "labels": "التسميات", - "degree": "الدرجة", - "properties": "الخصائص", - "relationships": "العلاقات (ضمن الرسم البياني الفرعي)", - "expandNode": "توسيع العقدة", - "pruneNode": "تقليم العقدة", - "deleteAllNodesError": "رفض حذف جميع العقد في الرسم البياني", - "nodesRemoved": "تم حذف {{count}} عقدة، بما في ذلك العقد اليتيمة", - "noNewNodes": "لم يتم العثور على عقد قابلة للتوسيع" - } - } - }, "documentPanel": { "clearDocuments": { "button": "مسح", @@ -262,6 +236,16 @@ "vectorStorage": "تخزين المتجهات" }, "propertiesView": { + "errors": { + "duplicateName": "اسم العقدة موجود بالفعل", + "updateFailed": "فشل تحديث العقدة", + "tryAgainLater": "يرجى المحاولة مرة أخرى لاحقًا" + }, + "doubleClickToEdit": "انقر نقرًا مزدوجًا للتعديل", + "success": { + "entityUpdated": "تم تحديث العقدة بنجاح", + "relationUpdated": "تم تحديث العلاقة بنجاح" + }, "node": { "title": "عقدة", "id": "المعرف", diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index a7119cbd..fb420ba1 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -240,6 +240,7 @@ "updateFailed": "Failed to update node", "tryAgainLater": "Please try again later" }, + "doubleClickToEdit": "Double click to edit", "success": { "entityUpdated": "Node updated successfully", "relationUpdated": "Relation updated successfully" diff --git a/lightrag_webui/src/locales/fr.json b/lightrag_webui/src/locales/fr.json index f4894d86..325d73d3 100644 --- a/lightrag_webui/src/locales/fr.json +++ b/lightrag_webui/src/locales/fr.json @@ -35,32 +35,7 @@ "common": { "cancel": "Annuler" }, - "graphPanel": { - "propertiesView": { - "errors": { - "duplicateName": "Le nom du nœud existe déjà", - "updateFailed": "Échec de la mise à jour du nœud", - "tryAgainLater": "Veuillez réessayer plus tard" - }, - "success": { - "entityUpdated": "Nœud mis à jour avec succès", - "relationUpdated": "Relation mise à jour avec succès" - }, - "node": { - "title": "Nœud", - "id": "ID", - "labels": "Étiquettes", - "degree": "Degré", - "properties": "Propriétés", - "relationships": "Relations(dans le sous-graphe)", - "expandNode": "Développer le nœud", - "pruneNode": "Élaguer le nœud", - "deleteAllNodesError": "Refus de supprimer tous les nœuds du graphe", - "nodesRemoved": "{{count}} nœuds supprimés, y compris les nœuds orphelins", - "noNewNodes": "Aucun nœud extensible trouvé" - } - } - }, + "documentPanel": { "clearDocuments": { "button": "Effacer", @@ -262,6 +237,16 @@ "vectorStorage": "Stockage vectoriel" }, "propertiesView": { + "errors": { + "duplicateName": "Le nom du nœud existe déjà", + "updateFailed": "Échec de la mise à jour du nœud", + "tryAgainLater": "Veuillez réessayer plus tard" + }, + "doubleClickToEdit": "Double-cliquez pour modifier", + "success": { + "entityUpdated": "Nœud mis à jour avec succès", + "relationUpdated": "Relation mise à jour avec succès" + }, "node": { "title": "Nœud", "id": "ID", diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index beeb56d4..1bbfbbf6 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -241,6 +241,7 @@ "updateFailed": "更新节点失败", "tryAgainLater": "请稍后重试" }, + "doubleClickToEdit": "双击编辑", "success": { "entityUpdated": "节点更新成功", "relationUpdated": "关系更新成功" From 5e5f3640d7dc90792555b32f505cb643c9ae8391 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sat, 12 Apr 2025 15:09:44 +0800 Subject: [PATCH 05/20] --- lightrag/api/routers/graph_routes.py | 34 +++ lightrag_webui/src/api/lightrag.ts | 4 +- .../components/graph/EditablePropertyRow.tsx | 276 +++++++++++------- .../src/components/graph/PropertiesView.tsx | 10 +- 4 files changed, 209 insertions(+), 115 deletions(-) diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index 97a397d4..e77e959a 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -17,6 +17,12 @@ class EntityUpdateRequest(BaseModel): allow_rename: bool = False +class RelationUpdateRequest(BaseModel): + source_id: str + target_id: str + updated_data: Dict[str, Any] + + def create_graph_routes(rag, api_key: Optional[str] = None): combined_auth = get_combined_auth_dependency(api_key) @@ -107,4 +113,32 @@ def create_graph_routes(rag, api_key: Optional[str] = None): status_code=500, detail=f"Error updating entity: {str(e)}" ) + @router.post("/graph/relation/edit", dependencies=[Depends(combined_auth)]) + async def update_relation(request: RelationUpdateRequest): + """Update a relation's properties in the knowledge graph + + Args: + request (RelationUpdateRequest): Request containing source ID, target ID and updated data + + Returns: + Dict: Updated relation information + """ + try: + result = await rag.aedit_relation( + source_entity=request.source_id, + target_entity=request.target_id, + updated_data=request.updated_data, + ) + return { + "status": "success", + "message": "Relation updated successfully", + "data": result, + } + except ValueError as ve: + raise HTTPException(status_code=400, detail=str(ve)) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error updating relation: {str(e)}" + ) + return router diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index 63f4e2f6..d0ca7d63 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -540,8 +540,8 @@ export const updateRelation = async ( updatedData: Record ): Promise => { const response = await axiosInstance.post('/graph/relation/edit', { - source_entity: sourceEntity, - target_entity: targetEntity, + source_id: sourceEntity, + target_id: targetEntity, updated_data: updatedData }) return response.data diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index bf583a0e..ac92b335 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -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(null) + // Update currentValue when initialValue changes + useEffect(() => { + setCurrentValue(initialValue) + }, [initialValue]) + // Initialize edit value when entering edit mode useEffect(() => { if (isEditing) { - setEditValue(String(value)) + setEditValue(String(currentValue)) // Focus the input element when entering edit mode setTimeout(() => { if (inputRef.current) { @@ -54,7 +61,7 @@ const EditablePropertyRow = ({ } }, 50) } - }, [isEditing, value]) + }, [isEditing, currentValue]) 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 - } - Object.entries(updatedAttributes).forEach(([key, value]) => { - sigmaGraph.setNodeAttribute(String(nodeId), key, value) + + // 更新边的引用关系 + 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 + } }) + 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(value)) { + if (editValue === String(currentValue)) { setIsEditing(false) return } @@ -150,112 +251,71 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { - const updatedData: Record = {} - 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')) - return - } - } - 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 updateGraphNode(entityId, name, editValue) + const exists = await checkEntityNameExists(editValue) + if (exists) { + toast.error(t('graphPanel.propertiesView.errors.duplicateName')) + setIsSubmitting(false) + return } + updatedData = { 'entity_name': editValue } } + 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 (onValueChange) { - onValueChange(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(false) + setCurrentValue(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 editable - // 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 state return ( -
-
- {getPropertyNameTranslation(name)} - {isEditableField && ( -
- -
- {t('graphPanel.propertiesView.doubleClickToEdit')} -
-
- )} +
+ {getPropertyNameTranslation(name)} +
+ setIsEditing(true)} + /> +
+ {t('graphPanel.propertiesView.doubleClickToEdit')} +
: {isEditing ? ( -
- setEditValue(e.target.value)} - onBlur={handleSave} - onKeyDown={handleKeyDown} - disabled={isSubmitting} - /> -
+ // Render input field when editing + setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + className="h-6 text-xs" + disabled={isSubmitting} + /> ) : ( -
+ // Render text component when not editing +
diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index c2cda263..257b69ec 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -195,8 +195,8 @@ const PropertyRow = ({ return translation === translationKey ? name : translation } - // Use EditablePropertyRow for editable fields (description and entity_id) - if (isEditable && (name === 'description' || name === 'entity_id')) { + // Use EditablePropertyRow for editable fields (description, entity_id and keywords) + if (isEditable && (name === 'description' || name === 'entity_id' || name === 'keywords')) { return ( { value={edge.properties[name]} entityId={edge.id} entityType="edge" - sourceId={edge.sourceNode?.properties['entity_id'] || edge.source} - targetId={edge.targetNode?.properties['entity_id'] || edge.target} - isEditable={name === 'description'} + sourceId={edge.source} + targetId={edge.target} + isEditable={name === 'description' || name === 'keywords'} /> ) })} From 830b69fd893cb58c79d147725bf41a9ba181b757 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sun, 13 Apr 2025 11:13:23 +0800 Subject: [PATCH 06/20] refactor(graph): Refactoring the EditablePeopleRow component --- .../components/graph/EditablePropertyRow.tsx | 223 +++++++++++++----- 1 file changed, 159 insertions(+), 64 deletions(-) diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index ac92b335..bfd88802 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -6,24 +6,39 @@ 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' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip' +/** + * Interface for the EditablePropertyRow component props + * Defines all possible properties that can be passed to the component + */ interface EditablePropertyRowProps { - name: string - value: any - onClick?: () => void - tooltip?: string - entityId?: string - entityType?: 'node' | 'edge' - sourceId?: string - targetId?: string - onValueChange?: (newValue: any) => void - isEditable?: boolean + name: string // Property name to display and edit + value: any // Initial value of the property + onClick?: () => void // Optional click handler for the property value + tooltip?: string // Optional tooltip text + entityId?: string // ID of the entity (for node type) + entityType?: 'node' | 'edge' // Type of graph entity + sourceId?: string // Source node ID (for edge type) + targetId?: string // Target node ID (for edge type) + onValueChange?: (newValue: any) => void // Optional callback when value changes + isEditable?: boolean // Whether this property can be edited +} + +/** + * Interface for tracking edges that need updating when a node ID changes + */ +interface EdgeToUpdate { + originalDynamicId: string; + newEdgeId: string; + edgeIndex: number; } /** * EditablePropertyRow component that supports double-click to edit property values - * Specifically designed for editing 'description' and entity name fields + * This component is used in the graph properties panel to display and edit entity properties + * + * @component */ const EditablePropertyRow = ({ name, @@ -37,6 +52,7 @@ const EditablePropertyRow = ({ onValueChange, isEditable = false }: EditablePropertyRowProps) => { + // Component state const { t } = useTranslation() const [isEditing, setIsEditing] = useState(false) const [editValue, setEditValue] = useState('') @@ -44,16 +60,21 @@ const EditablePropertyRow = ({ const [currentValue, setCurrentValue] = useState(initialValue) const inputRef = useRef(null) - // Update currentValue when initialValue changes + /** + * Update currentValue when initialValue changes from parent + */ useEffect(() => { setCurrentValue(initialValue) }, [initialValue]) - // Initialize edit value when entering edit mode + /** + * Initialize edit value and focus input when entering edit mode + */ useEffect(() => { if (isEditing) { setEditValue(String(currentValue)) - // Focus the input element when entering edit mode + // Focus the input element when entering edit mode with a small delay + // to ensure the input is rendered before focusing setTimeout(() => { if (inputRef.current) { inputRef.current.focus() @@ -63,18 +84,30 @@ const EditablePropertyRow = ({ } }, [isEditing, currentValue]) + /** + * Get translated property name from i18n + * Falls back to the original name if no translation is found + */ const getPropertyNameTranslation = (propName: string) => { const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}` const translation = t(translationKey) return translation === translationKey ? propName : translation } + /** + * Handle double-click event to enter edit mode + */ const handleDoubleClick = () => { if (isEditable && !isEditing) { setIsEditing(true) } } + /** + * Handle keyboard events in the input field + * - Enter: Save changes + * - Escape: Cancel editing + */ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSave() @@ -83,11 +116,21 @@ const EditablePropertyRow = ({ } } + /** + * Update node in the graph visualization after API update + * Handles both property updates and entity ID changes + * + * @param nodeId - ID of the node to update + * @param propertyName - Name of the property being updated + * @param newValue - New value for the property + */ const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => { + // Get graph state from store const sigmaInstance = useGraphStore.getState().sigmaInstance const sigmaGraph = useGraphStore.getState().sigmaGraph const rawGraph = useGraphStore.getState().rawGraph + // Validate graph state if (!sigmaInstance || !sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) { return } @@ -95,29 +138,30 @@ const EditablePropertyRow = ({ try { const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId)) + // Special handling for entity_id changes (node renaming) if (propertyName === 'entity_id') { + // Create new node with updated ID but same attributes sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) - interface EdgeToUpdate { - originalDynamicId: string; - newEdgeId: string; - edgeIndex: number; - } - const edgesToUpdate: EdgeToUpdate[] = []; + // Process all edges connected to this node sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => { const otherNode = source === String(nodeId) ? target : source const isOutgoing = source === String(nodeId) - // 获取原始边的dynamicId,以便后续更新edgeDynamicIdMap + // Get original edge dynamic ID for later reference const originalEdgeDynamicId = edge const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] - // 创建新边并获取新边的ID - const newEdgeId = sigmaGraph.addEdge(isOutgoing ? newValue : otherNode, isOutgoing ? otherNode : newValue, attributes) + // Create new edge with updated node reference + const newEdgeId = sigmaGraph.addEdge( + isOutgoing ? newValue : otherNode, + isOutgoing ? otherNode : newValue, + attributes + ) - // 存储需要更新的边信息 + // Track edges that need updating in the raw graph if (edgeIndexInRawGraph !== undefined) { edgesToUpdate.push({ originalDynamicId: originalEdgeDynamicId, @@ -126,11 +170,14 @@ const EditablePropertyRow = ({ }) } + // Remove the old edge sigmaGraph.dropEdge(edge) }) + // Remove the old node after all edges are processed sigmaGraph.dropNode(String(nodeId)) + // Update node reference in raw graph data const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] if (nodeIndex !== undefined) { rawGraph.nodes[nodeIndex].id = newValue @@ -139,10 +186,10 @@ const EditablePropertyRow = ({ rawGraph.nodeIdMap[newValue] = nodeIndex } - // 更新边的引用关系 + // Update all edge references in raw graph data edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { - // 更新边的source和target if (rawGraph.edges[edgeIndex]) { + // Update source/target references if (rawGraph.edges[edgeIndex].source === String(nodeId)) { rawGraph.edges[edgeIndex].source = newValue } @@ -150,23 +197,17 @@ const EditablePropertyRow = ({ rawGraph.edges[edgeIndex].target = newValue } - // 更新dynamicId映射 + // Update dynamic ID mappings rawGraph.edges[edgeIndex].dynamicId = newEdgeId delete rawGraph.edgeDynamicIdMap[originalDynamicId] rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex } }) - useGraphStore.getState().setSelectedNode(editValue) + // Update selected node in store + useGraphStore.getState().setSelectedNode(newValue) } else { - // const updatedAttributes = { ...nodeAttributes } - // if (propertyName === 'description') { - // updatedAttributes.description = newValue - // } - // Object.entries(updatedAttributes).forEach(([key, value]) => { - // sigmaGraph.setNodeAttribute(String(nodeId), key, value) - // }) - + // For other properties, just update the property in raw graph const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] if (nodeIndex !== undefined) { rawGraph.nodes[nodeIndex].properties[propertyName] = newValue @@ -178,16 +219,27 @@ const EditablePropertyRow = ({ } } + /** + * Update edge in the graph visualization after API update + * + * @param sourceId - ID of the source node + * @param targetId - ID of the target node + * @param propertyName - Name of the property being updated + * @param newValue - New value for the property + */ const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => { + // Get graph state from store const sigmaInstance = useGraphStore.getState().sigmaInstance const sigmaGraph = useGraphStore.getState().sigmaGraph const rawGraph = useGraphStore.getState().rawGraph + // Validate graph state if (!sigmaInstance || !sigmaGraph || !rawGraph) { return } try { + // Find the edge between source and target nodes const allEdges = sigmaGraph.edges() let keyToUse = null @@ -195,6 +247,7 @@ const EditablePropertyRow = ({ const edgeSource = sigmaGraph.source(edge) const edgeTarget = sigmaGraph.target(edge) + // Match edge in either direction (undirected graph support) if ((edgeSource === sourceId && edgeTarget === targetId) || (edgeSource === targetId && edgeTarget === sourceId)) { keyToUse = edge @@ -203,12 +256,14 @@ const EditablePropertyRow = ({ } if (keyToUse !== null) { + // Special handling for keywords property (updates edge label) if(propertyName === 'keywords') { sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue); } else { sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue); } + // Update edge in raw graph data using dynamic ID mapping if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) { const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]; if (rawGraph.edges[edgeIndex]) { @@ -217,32 +272,39 @@ const EditablePropertyRow = ({ 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'); - } + // Fallback: try to find edge by key in edge ID map + 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 + // Log the specific edge key that caused the error console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error); throw new Error('Failed to update edge in graph') } } + /** + * Save changes to the property value + * Updates both the API and the graph visualization + */ const handleSave = async () => { + // Prevent duplicate submissions if (isSubmitting) return + // Skip if value hasn't changed if (editValue === String(currentValue)) { setIsEditing(false) return @@ -251,30 +313,47 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { + // Handle node property updates if (entityType === 'node' && entityId) { let updatedData = { [name]: editValue } + // Special handling for entity_id (name) changes if (name === 'entity_id') { + // Check if the new name already exists const exists = await checkEntityNameExists(editValue) if (exists) { toast.error(t('graphPanel.propertiesView.errors.duplicateName')) setIsSubmitting(false) return } + // For entity_id, we update entity_name in the API updatedData = { 'entity_name': editValue } } + + // Update entity in API await updateEntity(entityId, updatedData, true) + // Update graph visualization await updateGraphNode(entityId, name, editValue) toast.success(t('graphPanel.propertiesView.success.entityUpdated')) - } else if (entityType === 'edge' && sourceId && targetId) { + } + // Handle edge property updates + else if (entityType === 'edge' && sourceId && targetId) { const updatedData = { [name]: editValue } + // Update relation in API await updateRelation(sourceId, targetId, updatedData) + // Update graph visualization await updateGraphEdge(sourceId, targetId, name, editValue) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } + // Update local state setIsEditing(false) setCurrentValue(editValue) + + // Notify parent component if callback provided + if (onValueChange) { + onValueChange(editValue) + } } catch (error) { console.error('Error updating property:', error) toast.error(t('graphPanel.propertiesView.errors.updateFailed')) @@ -283,21 +362,37 @@ const EditablePropertyRow = ({ } } - // Always render the property name label and edit icon, regardless of edit state + /** + * Render the property row with edit functionality + * Shows property name, edit icon, and either the editable input or the current value + */ return (
- {getPropertyNameTranslation(name)} -
- setIsEditing(true)} - /> -
- {t('graphPanel.propertiesView.doubleClickToEdit')} -
-
: + {/* Property name with translation */} + + {getPropertyNameTranslation(name)} + + + {/* Edit icon with tooltip */} + + + +
+ setIsEditing(true)} + /> +
+
+ + {t('graphPanel.propertiesView.doubleClickToEdit')} + +
+
: + + {/* Conditional rendering based on edit state */} {isEditing ? ( - // Render input field when editing + // Input field for editing ) : ( - // Render text component when not editing + // Text display when not editing
Date: Sun, 13 Apr 2025 11:48:55 +0800 Subject: [PATCH 07/20] bugfix --- lightrag_webui/src/lib/constants.ts | 2 +- lightrag_webui/src/locales/fr.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lightrag_webui/src/lib/constants.ts b/lightrag_webui/src/lib/constants.ts index 87db8cea..048ae8f7 100644 --- a/lightrag_webui/src/lib/constants.ts +++ b/lightrag_webui/src/lib/constants.ts @@ -1,6 +1,6 @@ import { ButtonVariantType } from '@/components/ui/Button' -export const backendBaseUrl = 'http://localhost:9621' +export const backendBaseUrl = '' export const webuiPrefix = '/webui/' export const controlButtonVariant: ButtonVariantType = 'ghost' diff --git a/lightrag_webui/src/locales/fr.json b/lightrag_webui/src/locales/fr.json index 325d73d3..1217ba66 100644 --- a/lightrag_webui/src/locales/fr.json +++ b/lightrag_webui/src/locales/fr.json @@ -35,7 +35,6 @@ "common": { "cancel": "Annuler" }, - "documentPanel": { "clearDocuments": { "button": "Effacer", From 5b1938e5b384c5efb3f98bb6defeca5bf2cc5ad2 Mon Sep 17 00:00:00 2001 From: choizhang Date: Sun, 13 Apr 2025 23:32:35 +0800 Subject: [PATCH 08/20] feat(webui): Add attribute editing dialog box and optimize editable attribute row component --- lightrag/api/routers/graph_routes.py | 1 - .../components/graph/EditablePropertyRow.tsx | 121 +++++++----------- .../components/graph/PropertyEditDialog.tsx | 105 +++++++++++++++ lightrag_webui/src/locales/ar.json | 7 +- lightrag_webui/src/locales/en.json | 7 +- lightrag_webui/src/locales/fr.json | 5 +- lightrag_webui/src/locales/zh.json | 7 +- 7 files changed, 173 insertions(+), 80 deletions(-) create mode 100644 lightrag_webui/src/components/graph/PropertyEditDialog.tsx diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index e77e959a..107a7952 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -95,7 +95,6 @@ def create_graph_routes(rag, api_key: Optional[str] = None): Dict: Updated entity information """ try: - print(request.entity_name, request.updated_data, request.allow_rename) result = await rag.aedit_entity( entity_name=request.entity_name, updated_data=request.updated_data, diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index bfd88802..bca482b8 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -1,12 +1,12 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect } from 'react' +import { useRef } from 'react' import { useTranslation } from 'react-i18next' import Text from '@/components/ui/Text' -import Input from '@/components/ui/Input' import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' import { PencilIcon } from 'lucide-react' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip' +import PropertyEditDialog from './PropertyEditDialog' /** * Interface for the EditablePropertyRow component props @@ -44,7 +44,6 @@ const EditablePropertyRow = ({ name, value: initialValue, onClick, - tooltip, entityId, entityType, sourceId, @@ -55,11 +54,11 @@ const EditablePropertyRow = ({ // Component state const { t } = useTranslation() const [isEditing, setIsEditing] = useState(false) - const [editValue, setEditValue] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [currentValue, setCurrentValue] = useState(initialValue) const inputRef = useRef(null) + /** * Update currentValue when initialValue changes from parent */ @@ -72,7 +71,6 @@ const EditablePropertyRow = ({ */ useEffect(() => { if (isEditing) { - setEditValue(String(currentValue)) // Focus the input element when entering edit mode with a small delay // to ensure the input is rendered before focusing setTimeout(() => { @@ -82,7 +80,7 @@ const EditablePropertyRow = ({ } }, 50) } - }, [isEditing, currentValue]) + }, [isEditing]) /** * Get translated property name from i18n @@ -95,25 +93,19 @@ const EditablePropertyRow = ({ } /** - * Handle double-click event to enter edit mode + * Handle edit icon click to open dialog */ - const handleDoubleClick = () => { + const handleEditClick = () => { if (isEditable && !isEditing) { setIsEditing(true) } } /** - * Handle keyboard events in the input field - * - Enter: Save changes - * - Escape: Cancel editing + * Handle dialog close without saving */ - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSave() - } else if (e.key === 'Escape') { - setIsEditing(false) - } + const handleCancel = () => { + setIsEditing(false) } /** @@ -300,12 +292,12 @@ const EditablePropertyRow = ({ * Save changes to the property value * Updates both the API and the graph visualization */ - const handleSave = async () => { + const handleSave = async (value: string) => { // Prevent duplicate submissions if (isSubmitting) return // Skip if value hasn't changed - if (editValue === String(currentValue)) { + if (value === String(currentValue)) { setIsEditing(false) return } @@ -315,44 +307,44 @@ const EditablePropertyRow = ({ try { // Handle node property updates if (entityType === 'node' && entityId) { - let updatedData = { [name]: editValue } + let updatedData = { [name]: value } // Special handling for entity_id (name) changes if (name === 'entity_id') { // Check if the new name already exists - const exists = await checkEntityNameExists(editValue) + const exists = await checkEntityNameExists(value) if (exists) { toast.error(t('graphPanel.propertiesView.errors.duplicateName')) setIsSubmitting(false) return } // For entity_id, we update entity_name in the API - updatedData = { 'entity_name': editValue } + updatedData = { 'entity_name': value } } // Update entity in API await updateEntity(entityId, updatedData, true) // Update graph visualization - await updateGraphNode(entityId, name, editValue) + await updateGraphNode(entityId, name, value) toast.success(t('graphPanel.propertiesView.success.entityUpdated')) } // Handle edge property updates else if (entityType === 'edge' && sourceId && targetId) { - const updatedData = { [name]: editValue } + const updatedData = { [name]: value } // Update relation in API await updateRelation(sourceId, targetId, updatedData) // Update graph visualization - await updateGraphEdge(sourceId, targetId, name, editValue) + await updateGraphEdge(sourceId, targetId, name, value) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } // Update local state setIsEditing(false) - setCurrentValue(editValue) + setCurrentValue(value) // Notify parent component if callback provided if (onValueChange) { - onValueChange(editValue) + onValueChange(value) } } catch (error) { console.error('Error updating property:', error) @@ -364,58 +356,43 @@ const EditablePropertyRow = ({ /** * Render the property row with edit functionality - * Shows property name, edit icon, and either the editable input or the current value + * Shows property name, edit icon, and the current value */ return ( -
+
{/* Property name with translation */} {getPropertyNameTranslation(name)} - {/* Edit icon with tooltip */} - - - -
- setIsEditing(true)} - /> -
-
- - {t('graphPanel.propertiesView.doubleClickToEdit')} - -
-
: - - {/* Conditional rendering based on edit state */} - {isEditing ? ( - // Input field for editing - setEditValue(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSave} - className="h-6 text-xs" - disabled={isSubmitting} + {/* Edit icon without tooltip */} +
+ - ) : ( - // Text display when not editing -
- -
- )} +
: + + {/* Text display */} +
+ +
+ + {/* Edit dialog */} +
) } diff --git a/lightrag_webui/src/components/graph/PropertyEditDialog.tsx b/lightrag_webui/src/components/graph/PropertyEditDialog.tsx new file mode 100644 index 00000000..612ffaf9 --- /dev/null +++ b/lightrag_webui/src/components/graph/PropertyEditDialog.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter +} from '@/components/ui/Dialog' +import Button from '@/components/ui/Button' +import Input from '@/components/ui/Input' + +interface PropertyEditDialogProps { + isOpen: boolean + onClose: () => void + onSave: (value: string) => void + propertyName: string + initialValue: string + isSubmitting?: boolean +} + +/** + * Dialog component for editing property values + * Provides a modal with a title, multi-line text input, and save/cancel buttons + */ +const PropertyEditDialog = ({ + isOpen, + onClose, + onSave, + propertyName, + initialValue, + isSubmitting = false +}: PropertyEditDialogProps) => { + const { t } = useTranslation() + const [value, setValue] = useState('') + + // Initialize value when dialog opens + useEffect(() => { + if (isOpen) { + setValue(initialValue) + } + }, [isOpen, initialValue]) + + // Get translated property name + const getPropertyNameTranslation = (name: string) => { + const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}` + const translation = t(translationKey) + return translation === translationKey ? name : translation + } + + const handleSave = () => { + if (value.trim() !== '') { + onSave(value) + onClose() + } +} + + return ( + !open && onClose()}> + + + + {t('graphPanel.propertiesView.editProperty', { + property: getPropertyNameTranslation(propertyName) + })} + +

+ {t('graphPanel.propertiesView.editPropertyDescription')} +

+
+ + {/* Multi-line text input using textarea */} +
+