EditModeOverlay.tsx
1 import React from 'react'; 2 import { useInterBrainStore } from '../../store/interbrain-store'; 3 import { serviceManager } from '../../services/service-manager'; 4 import { UIService } from '../../services/ui-service'; 5 import { DreamNode } from '../../types/dreamnode'; 6 import EditNode3D from './EditNode3D'; 7 import EditModeSearchNode3D from './EditModeSearchNode3D'; 8 9 // Create UIService instance for showing user messages 10 const uiService = new UIService(); 11 12 /** 13 * EditModeOverlay - Main coordinator for edit mode functionality 14 * 15 * Integrates with existing spatial layout system: 16 * - EditNode3D for metadata editing at center position 17 * - Leverages existing SpatialOrchestrator search layout for relationship nodes 18 * - Uses existing DreamNode3D components with gold glow for relationships 19 */ 20 export default function EditModeOverlay() { 21 const { 22 editMode, 23 savePendingRelationships, 24 exitEditMode 25 } = useInterBrainStore(); 26 27 // Center position for the editing node (similar to ProtoNode spawn position) 28 const centerPosition: [number, number, number] = [0, 0, -50]; 29 30 // Handler functions defined before useEffect to maintain hook order 31 const handleCancel = () => { 32 // Exit edit mode - original data is preserved in store 33 exitEditMode(); 34 35 // Return to liminal-web layout (edit mode requires a selected node) 36 const store = useInterBrainStore.getState(); 37 if (store.selectedNode) { 38 store.setSpatialLayout('liminal-web'); 39 } 40 }; 41 42 // Global escape key handling is now managed by DreamspaceCanvas for stability 43 44 // Don't render if edit mode is not active 45 if (!editMode.isActive || !editMode.editingNode) { 46 return null; 47 } 48 49 const handleSave = async () => { 50 try { 51 if (!editMode.editingNode) { 52 console.error('No editing node available for save operation'); 53 return; 54 } 55 56 // Get the active service for persistence 57 const dreamNodeService = serviceManager.getActive(); 58 59 // 1. Handle new DreamTalk media file if provided 60 if (editMode.newDreamTalkFile) { 61 console.log(`EditModeOverlay: Saving new DreamTalk media: ${editMode.newDreamTalkFile.name}`); 62 await dreamNodeService.addFilesToNode(editMode.editingNode.id, [editMode.newDreamTalkFile]); 63 } 64 65 // 2. Save metadata changes (let service layer handle if no changes) 66 const updates: Partial<DreamNode> = { 67 name: editMode.editingNode.name, 68 type: editMode.editingNode.type 69 }; 70 71 // Include contact info only for dreamer-type nodes 72 if (editMode.editingNode.type === 'dreamer') { 73 updates.email = editMode.editingNode.email; 74 updates.phone = editMode.editingNode.phone; 75 updates.radicleId = editMode.editingNode.radicleId; 76 } 77 78 await dreamNodeService.update(editMode.editingNode.id, updates); 79 80 // 3. Save relationship changes through service layer 81 await dreamNodeService.updateRelationships( 82 editMode.editingNode.id, 83 editMode.pendingRelationships 84 ); 85 86 // 4. Update store state with confirmed relationships 87 savePendingRelationships(); 88 89 // 5. Clear the new DreamTalk file from edit mode state (successful save) 90 if (editMode.newDreamTalkFile) { 91 useInterBrainStore.getState().setEditModeNewDreamTalkFile(undefined); 92 } 93 94 console.log(`Edit mode changes saved for node ${editMode.editingNode.id}:`, { 95 relationships: editMode.pendingRelationships.length, 96 dataMode: serviceManager.getMode() 97 }); 98 99 // Update the selected node in the main store with the latest data 100 const freshNode = await dreamNodeService.get(editMode.editingNode.id); 101 if (freshNode) { 102 // Update the selectedNode to reflect the saved changes 103 useInterBrainStore.getState().setSelectedNode(freshNode); 104 105 // Immediately trigger the special edit mode save transition 106 // This starts the parallel animations: UI fade + node movements 107 const canvas = globalThis.document.querySelector('[data-dreamspace-canvas]'); 108 if (canvas) { 109 const event = new globalThis.CustomEvent('edit-mode-save-transition', { 110 detail: { nodeId: freshNode.id } 111 }); 112 canvas.dispatchEvent(event); 113 } 114 115 // Exit edit mode after animations complete 116 // Don't change spatial layout - it's already handled by the event 117 globalThis.setTimeout(() => { 118 exitEditMode(); 119 }, 1000); 120 } 121 } catch (error) { 122 console.error('Failed to save edit mode changes:', error); 123 console.log('Edit mode state during error:', { 124 isActive: editMode.isActive, 125 hasEditingNode: !!editMode.editingNode, 126 editingNodeName: editMode.editingNode?.name 127 }); 128 129 // Show user-friendly error message in Obsidian UI 130 uiService.showError('Failed to save changes: ' + (error instanceof Error ? error.message : 'Unknown error')); 131 132 // Ensure edit mode state is preserved after error 133 console.log('Edit mode state after error message:', { 134 isActive: useInterBrainStore.getState().editMode.isActive, 135 hasEditingNode: !!useInterBrainStore.getState().editMode.editingNode 136 }); 137 138 // Don't exit edit mode if save fails - user can try again or cancel 139 } 140 }; 141 142 const handleSearchToggleOff = async () => { 143 const store = useInterBrainStore.getState(); 144 145 console.log(`🔍 [EditModeOverlay] Toggling off search mode - filtering to pending relationships`); 146 147 // Close the search interface 148 store.setEditModeSearchActive(false); 149 150 // Get current pending relationships to show only related nodes 151 const pendingRelationshipIds = store.editMode.pendingRelationships; 152 153 if (pendingRelationshipIds.length > 0 && store.editMode.editingNode) { 154 // Get the related nodes from the service layer 155 const dreamNodeService = serviceManager.getActive(); 156 const relatedNodes = await Promise.all( 157 pendingRelationshipIds.map(id => dreamNodeService.get(id)) 158 ); 159 160 // Filter out any null results (in case some relationships are broken) 161 const validRelatedNodes = relatedNodes.filter(node => node !== null) as DreamNode[]; 162 163 console.log(`✅ [EditModeOverlay] Showing ${validRelatedNodes.length} pending related nodes after search toggle off`); 164 165 // Update the edit mode search results to show only pending relationships 166 store.setEditModeSearchResults(validRelatedNodes); 167 168 // CRITICAL: Clear stale orchestrator data before showing filtered results 169 const canvas = globalThis.document.querySelector('[data-dreamspace-canvas]'); 170 if (canvas) { 171 // First clear the stale edit mode data 172 const clearEvent = new globalThis.CustomEvent('clear-edit-mode-data', { 173 detail: { source: 'search-toggle-off' } 174 }); 175 canvas.dispatchEvent(clearEvent); 176 177 // Then trigger the filtered layout after a brief delay to ensure cleanup completes 178 globalThis.setTimeout(() => { 179 if (store.editMode.editingNode) { 180 const layoutEvent = new globalThis.CustomEvent('edit-mode-search-layout', { 181 detail: { 182 centerNodeId: store.editMode.editingNode.id, 183 searchResults: validRelatedNodes 184 } 185 }); 186 canvas.dispatchEvent(layoutEvent); 187 } 188 }, 10); 189 } 190 } else { 191 console.log(`📭 [EditModeOverlay] No pending relationships - clearing search results`); 192 // No pending relationships, clear search results 193 store.setEditModeSearchResults([]); 194 } 195 196 // Note: Focus management removed - global DreamspaceCanvas escape handler doesn't require it 197 }; 198 199 200 201 return ( 202 <group> 203 {/* Central metadata editing interface */} 204 <EditNode3D 205 position={centerPosition} 206 onSave={handleSave} 207 onCancel={handleCancel} 208 onToggleSearchOff={handleSearchToggleOff} 209 /> 210 211 {/* Relationship search interface - renders on top of EditNode3D when active */} 212 {editMode.isSearchingRelationships && ( 213 <EditModeSearchNode3D 214 position={centerPosition} 215 /> 216 )} 217 218 {/* Relationship management now handled by existing SpatialOrchestrator + DreamNode3D components */} 219 {/* Gold glow and click handling will be added to existing DreamNode3D components */} 220 </group> 221 ); 222 }