import { app } from "/scripts/app.js"; // Store notes data for each node // Format: nodeNotes[nodeId] = [ { text: "note text", date: "2023-04-15 10:30" }, ... ] const nodeNotes = {}; // Flag to control notes visibility let notesVisible = true; // Create UI for notes modal function createNotesUI() { // Create modal container if it doesn't exist if (!document.getElementById('notes-modal-container')) { const modalContainer = document.createElement('div'); modalContainer.id = 'notes-modal-container'; modalContainer.className = 'notes-modal-container'; modalContainer.style.display = 'none'; // Modal content modalContainer.innerHTML = `

Node Notes

`; document.body.appendChild(modalContainer); // Add styles const style = document.createElement('style'); style.textContent = ` .notes-modal-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10000; } .notes-modal { background-color: #222; border-radius: 8px; width: 500px; max-width: 90%; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); border: 1px solid #444; } .notes-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #444; background-color: #333; border-radius: 8px 8px 0 0; } .notes-modal-header h3 { margin: 0; color: #FF9800; font-size: 18px; } .notes-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #aaa; } .notes-modal-close:hover { color: #FF9800; } .notes-modal-body { padding: 15px; overflow-y: auto; max-height: 60vh; flex-grow: 1; } .notes-modal-footer { padding: 15px; display: flex; justify-content: space-between; border-top: 1px solid #444; background-color: #333; border-radius: 0 0 8px 8px; } .notes-modal-footer-right { display: flex; gap: 10px; justify-content: flex-end; } .notes-modal-btn { padding: 8px 16px; background-color: #444; border: none; border-radius: 4px; color: white; cursor: pointer; transition: background-color 0.2s; } .notes-modal-btn:hover { background-color: #555; } #notes-add-btn, #notes-save-btn { background-color: #FF9800; color: #111; } #notes-add-btn:hover, #notes-save-btn:hover { background-color: #FFA726; } .notes-dropdown { position: relative; display: inline-block; } .notes-dropdown-btn { padding: 8px 16px; background-color: #444; border: none; border-radius: 4px; color: white; cursor: pointer; transition: background-color 0.2s; } .notes-dropdown-btn:hover { background-color: #555; } .notes-dropdown-content { display: none; position: absolute; bottom: 40px; left: 0; background-color: #333; min-width: 180px; box-shadow: 0 8px 16px rgba(0,0,0,0.3); z-index: 1; border-radius: 4px; border: 1px solid #444; } .notes-dropdown:hover .notes-dropdown-content { display: block; } .notes-dropdown-content button { width: 100%; text-align: left; padding: 10px 15px; background: none; border: none; color: white; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } .notes-dropdown-content button:hover { background-color: #444; } #notes-edit-textarea { width: 100%; min-height: 150px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; padding: 10px; resize: vertical; font-family: inherit; font-size: 14px; } .note-item { background-color: #333; border-radius: 4px; padding: 10px 15px; margin-bottom: 10px; border-left: 3px solid #FF9800; } .note-header { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 12px; color: #aaa; } .note-actions { display: flex; gap: 5px; } .note-action-btn { background: none; border: none; color: #aaa; cursor: pointer; font-size: 14px; padding: 0 5px; } .note-action-btn:hover { color: #FF9800; } .note-content { white-space: pre-wrap; color: white; font-size: 14px; } .note-item:last-child { margin-bottom: 0; } `; document.head.appendChild(style); // Event listeners for the modal setupModalEventListeners(); // Initialize dropdown behavior initDropdownBehavior(); } } // Set up event listeners for modal buttons function setupModalEventListeners() { // Close modal when clicking close button document.querySelector('.notes-modal-close').addEventListener('click', closeModal); document.getElementById('notes-close-btn').addEventListener('click', closeModal); // Close modal when clicking outside document.getElementById('notes-modal-container').addEventListener('click', function(e) { if (e.target === this) { closeModal(); } }); // Add note button document.getElementById('notes-add-btn').addEventListener('click', function() { showEditMode(); }); // Save button document.getElementById('notes-save-btn').addEventListener('click', saveNote); // Cancel button document.getElementById('notes-cancel-btn').addEventListener('click', function() { showViewMode(); }); // Export/Import buttons document.getElementById('notes-export-btn').addEventListener('click', function() { exportNotes(true); // Export for current node only }); document.getElementById('notes-import-btn').addEventListener('click', function() { importNotes(true); // Import for current node only }); document.getElementById('notes-export-all-btn').addEventListener('click', function() { exportNotes(false); // Export all nodes }); } // Initialize dropdown behavior function initDropdownBehavior() { // Make sure the dropdown is closed when clicking outside document.addEventListener('click', function(event) { if (!event.target.matches('.notes-dropdown-btn') && !event.target.closest('.notes-dropdown-content')) { const dropdowns = document.getElementsByClassName('notes-dropdown-content'); for (let i = 0; i < dropdowns.length; i++) { const openDropdown = dropdowns[i]; if (openDropdown.style.display === 'block') { openDropdown.style.display = 'none'; } } } }); // Toggle dropdown when clicking the button document.querySelectorAll('.notes-dropdown-btn').forEach(btn => { btn.addEventListener('click', function() { const content = this.nextElementSibling; content.style.display = content.style.display === 'block' ? 'none' : 'block'; }); }); } // Open modal with notes for a node function openNotesModal(node) { createNotesUI(); // Make sure UI is created const modal = document.getElementById('notes-modal-container'); const title = document.getElementById('notes-modal-title'); const nodeTitle = node.title || `Node #${node.id}`; // Set title and show the modal title.textContent = `Notes: ${nodeTitle}`; modal.style.display = 'flex'; // Store reference to current node modal.dataset.nodeId = node.id; // Show notes in view mode showViewMode(); renderNotes(node); } // Close the modal function closeModal() { const modal = document.getElementById('notes-modal-container'); if (modal) { modal.style.display = 'none'; } } // Show modal in view mode function showViewMode() { document.getElementById('notes-view-container').style.display = 'block'; document.getElementById('notes-edit-container').style.display = 'none'; document.getElementById('notes-add-btn').style.display = 'inline-block'; document.getElementById('notes-save-btn').style.display = 'none'; document.getElementById('notes-cancel-btn').style.display = 'none'; document.getElementById('notes-close-btn').style.display = 'inline-block'; } // Show modal in edit mode function showEditMode(noteText = '', editIndex = -1) { document.getElementById('notes-view-container').style.display = 'none'; document.getElementById('notes-edit-container').style.display = 'block'; document.getElementById('notes-add-btn').style.display = 'none'; document.getElementById('notes-save-btn').style.display = 'inline-block'; document.getElementById('notes-cancel-btn').style.display = 'inline-block'; document.getElementById('notes-close-btn').style.display = 'none'; const textarea = document.getElementById('notes-edit-textarea'); textarea.value = noteText; textarea.focus(); // Store edit index in the textarea dataset textarea.dataset.editIndex = editIndex; } // Get current node from modal function getCurrentNode() { const modal = document.getElementById('notes-modal-container'); const nodeId = parseInt(modal.dataset.nodeId); return app.graph._nodes_by_id[nodeId]; } // Render all notes for a node function renderNotes(node) { const notesList = document.getElementById('notes-list'); notesList.innerHTML = ''; if (!node.notes || node.notes.length === 0) { notesList.innerHTML = '
No notes yet. Click "Add Note" to create one.
'; return; } // Display notes from newest to oldest for (let i = node.notes.length - 1; i >= 0; i--) { const note = node.notes[i]; const noteElement = document.createElement('div'); noteElement.className = 'note-item'; noteElement.innerHTML = `
${note.date}
${escapeHTML(note.text)}
`; notesList.appendChild(noteElement); } // Add event listeners to edit and delete buttons notesList.querySelectorAll('.edit-note').forEach(button => { button.addEventListener('click', function() { const index = parseInt(this.dataset.index); editExistingNote(index); }); }); notesList.querySelectorAll('.delete-note').forEach(button => { button.addEventListener('click', function() { const index = parseInt(this.dataset.index); deleteNote(index); }); }); } // Edit an existing note function editExistingNote(index) { const node = getCurrentNode(); if (node.notes && index < node.notes.length) { showEditMode(node.notes[index].text, index); } } // Save the current note function saveNote() { const node = getCurrentNode(); const textarea = document.getElementById('notes-edit-textarea'); const noteText = textarea.value.trim(); const editIndex = parseInt(textarea.dataset.editIndex); if (noteText) { const date = new Date(); const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; const note = { text: noteText, date: editIndex >= 0 ? `${formattedDate} (edited)` : formattedDate }; if (!node.notes) { node.notes = []; } if (editIndex >= 0 && editIndex < node.notes.length) { // Edit existing note node.notes[editIndex] = note; } else { // Add new note node.notes.push(note); } // Update global notes storage const noteId = node.id.toString(); nodeNotes[noteId] = [...node.notes]; // Update icon visibility if (node.notes.length > 0) { node.flags.hasNotes = true; } // Mark graph as dirty to ensure it gets saved if (app.graph) { app.graph.change(); } // Switch back to view mode and render updated notes showViewMode(); renderNotes(node); } } // Delete a note function deleteNote(index) { if (!confirm("Delete this note?")) return; const node = getCurrentNode(); if (node.notes && index < node.notes.length) { node.notes.splice(index, 1); // Update global notes storage const noteId = node.id.toString(); nodeNotes[noteId] = [...node.notes]; // Hide icon if there are no more notes if (node.notes.length === 0) { node.flags.hasNotes = false; } // Mark graph as dirty to ensure it gets saved if (app.graph) { app.graph.change(); } // Render updated notes renderNotes(node); } } // Helper function to escape HTML content function escapeHTML(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Export notes to a JSON file function exportNotes(nodeOnly = false) { if (nodeOnly) { // Export selected notes for the current node showExportSelectionModal(true); } else { // Export selected notes from all nodes showExportSelectionModal(false); } } // Show modal for selecting notes to export function showExportSelectionModal(nodeOnly = false) { // Create a container for the export selection modal const modalContainer = document.createElement('div'); modalContainer.id = 'notes-export-modal-container'; modalContainer.className = 'notes-modal-container'; let notesToExport = []; if (nodeOnly) { // Get notes for current node only const node = getCurrentNode(); if (!node || !node.notes || node.notes.length === 0) { alert("No notes to export for this node."); return; } notesToExport.push({ nodeId: node.id, nodeTitle: node.title || `Node #${node.id}`, notes: node.notes }); } else { // Get all notes if (Object.keys(nodeNotes).length === 0) { alert("No notes to export."); return; } // Collect all notes with node information for (const nodeId in nodeNotes) { if (Array.isArray(nodeNotes[nodeId]) && nodeNotes[nodeId].length > 0) { const node = app.graph._nodes_by_id[parseInt(nodeId)]; notesToExport.push({ nodeId: nodeId, nodeTitle: node ? (node.title || `Node #${nodeId}`) : `Node #${nodeId}`, notes: nodeNotes[nodeId] }); } } } // Create the modal content modalContainer.innerHTML = `

Select Notes to Export

${notesToExport.map((nodeData, nodeIdx) => `
${nodeData.notes.map((note, noteIdx) => `
`).join('')}
`).join('')}
`; document.body.appendChild(modalContainer); // Add additional styles for the export selection modal const style = document.createElement('style'); style.textContent = ` #export-selection-container { max-height: 60vh; overflow-y: auto; } .export-node-section { margin-bottom: 20px; border: 1px solid #444; border-radius: 4px; overflow: hidden; } .export-node-header { background-color: #333; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; } .export-note-items { padding: 0 15px; } .export-note-item { padding: 8px 0; border-bottom: 1px solid #444; } .export-note-item:last-child { border-bottom: none; } .export-checkbox-label { display: flex; align-items: flex-start; cursor: pointer; } .export-checkbox-label input[type="checkbox"] { margin-top: 3px; margin-right: 10px; } .export-note-preview { color: #ccc; font-size: 13px; white-space: pre-line; } .toggle-selection-btn { background-color: #444; border: none; color: white; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; } .toggle-selection-btn:hover { background-color: #555; } `; document.head.appendChild(style); // Set up event listeners for the export selection modal setupExportSelectionModalEventListeners(notesToExport, nodeOnly); } function setupExportSelectionModalEventListeners(notesToExport, nodeOnly) { const modalContainer = document.getElementById('notes-export-modal-container'); // Close button modalContainer.querySelector('.notes-modal-close').addEventListener('click', function() { modalContainer.remove(); }); // Cancel button document.getElementById('cancel-export-btn').addEventListener('click', function() { modalContainer.remove(); }); // Select All button document.getElementById('select-all-notes-btn').addEventListener('click', function() { modalContainer.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { checkbox.checked = true; }); }); // Deselect All button document.getElementById('deselect-all-notes-btn').addEventListener('click', function() { modalContainer.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { checkbox.checked = false; }); }); // Toggle All buttons for each node modalContainer.querySelectorAll('.toggle-selection-btn').forEach(button => { button.addEventListener('click', function() { const nodeIdx = this.dataset.nodeIdx; const nodeCheckbox = modalContainer.querySelector(`.export-node-checkbox[data-node-idx="${nodeIdx}"]`); const noteCheckboxes = modalContainer.querySelectorAll(`.export-note-checkbox[data-node-idx="${nodeIdx}"]`); // Check if all note checkboxes are currently checked const allChecked = Array.from(noteCheckboxes).every(cb => cb.checked); // Toggle all checkboxes noteCheckboxes.forEach(checkbox => { checkbox.checked = !allChecked; }); // Update node checkbox based on note checkboxes nodeCheckbox.checked = !allChecked && Array.from(noteCheckboxes).some(cb => cb.checked); }); }); // Node checkboxes control note checkboxes modalContainer.querySelectorAll('.export-node-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { const nodeIdx = this.dataset.nodeIdx; const noteCheckboxes = modalContainer.querySelectorAll(`.export-note-checkbox[data-node-idx="${nodeIdx}"]`); noteCheckboxes.forEach(noteCheckbox => { noteCheckbox.checked = this.checked; }); }); }); // Note checkboxes affect node checkbox modalContainer.querySelectorAll('.export-note-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { const nodeIdx = this.dataset.nodeIdx; const nodeCheckbox = modalContainer.querySelector(`.export-node-checkbox[data-node-idx="${nodeIdx}"]`); const noteCheckboxes = modalContainer.querySelectorAll(`.export-note-checkbox[data-node-idx="${nodeIdx}"]`); // If at least one note is checked, the node checkbox should be checked // If no notes are checked, the node checkbox should be unchecked nodeCheckbox.checked = Array.from(noteCheckboxes).some(cb => cb.checked); }); }); // Export Selected button document.getElementById('export-selected-btn').addEventListener('click', function() { // Collect selected notes let exportData = {}; notesToExport.forEach((nodeData, nodeIdx) => { const nodeCheckbox = modalContainer.querySelector(`.export-node-checkbox[data-node-idx="${nodeIdx}"]`); if (nodeCheckbox.checked) { const selectedNotes = []; nodeData.notes.forEach((note, noteIdx) => { const noteCheckbox = modalContainer.querySelector(`.export-note-checkbox[data-node-idx="${nodeIdx}"][data-note-idx="${noteIdx}"]`); if (noteCheckbox && noteCheckbox.checked) { selectedNotes.push(note); } }); if (selectedNotes.length > 0) { exportData[nodeData.nodeId] = selectedNotes; } } }); if (Object.keys(exportData).length === 0) { alert("No notes selected for export."); return; } // Create a download link and trigger the download const fileName = nodeOnly ? `notes_node_${getCurrentNode().id}.json` : "node_notes.json"; const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", fileName); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); // Close the modal modalContainer.remove(); }); } // Import notes from a JSON file function importNotes() { // Create file input element const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.onchange = function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { try { const importData = JSON.parse(event.target.result); const node = getCurrentNode(); const nodeId = node.id.toString(); // Check if the imported data is an array (notes for a single node) if (Array.isArray(importData)) { node.notes = importData; nodeNotes[nodeId] = [...importData]; } // Check if the imported data is an object with node IDs else if (typeof importData === 'object' && importData !== null) { // Check if current node ID exists in imported data if (importData[nodeId] && Array.isArray(importData[nodeId])) { node.notes = [...importData[nodeId]]; nodeNotes[nodeId] = [...importData[nodeId]]; } // If not, take the first node's notes else { const firstNodeId = Object.keys(importData)[0]; if (firstNodeId && Array.isArray(importData[firstNodeId])) { node.notes = [...importData[firstNodeId]]; nodeNotes[nodeId] = [...importData[firstNodeId]]; } else { throw new Error("Invalid import format. Expected an array of notes or an object with node IDs."); } } } else { throw new Error("Invalid import format. Expected an array of notes or an object with node IDs."); } // Update node display if (node.notes.length > 0) { node.flags.hasNotes = true; } else { node.flags.hasNotes = false; } // Refresh the notes display renderNotes(node); // Mark graph as dirty to ensure it gets saved if (app.graph) { app.graph.change(); } } catch (error) { console.error("Error importing notes:", error); alert("Error importing notes: " + error.message); } }; reader.readAsText(file); }; // Trigger file selection fileInput.click(); } // Create the draggable notes icon function createNotesIcon() { const style = document.createElement('style'); style.textContent = ` #notes-icon { position: fixed; left: 15px; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; background: #1a1a1a; border-radius: 4px; border: 1px solid #333; color: #ddd; font-size: 20px; display: flex; align-items: center; justify-content: center; cursor: move; z-index: 999; transition: all 0.2s; user-select: none; } #notes-icon:hover { background: #333; color: #fff; } #notes-icon.active { color: #FF9800; border-color: #FF9800; } `; document.head.appendChild(style); const notesIcon = document.createElement('div'); notesIcon.id = 'notes-icon'; notesIcon.innerHTML = '📋'; notesIcon.title = 'Toggle Notes Panel'; notesIcon.addEventListener('click', function(e) { if (!notesIcon.isDragging) { toggleGlobalNotesPanel(); notesIcon.classList.toggle('active', document.getElementById('global-notes-panel-container').style.display === 'flex'); } notesIcon.isDragging = false; }); // Add drag functionality let offsetX, offsetY; let isDragging = false; notesIcon.addEventListener('mousedown', function(e) { offsetX = e.clientX - notesIcon.getBoundingClientRect().left; offsetY = e.clientY - notesIcon.getBoundingClientRect().top; isDragging = true; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (isDragging) { const x = e.clientX - offsetX; const y = e.clientY - offsetY; notesIcon.style.left = `${x}px`; notesIcon.style.top = `${y}px`; notesIcon.style.transform = 'none'; notesIcon.isDragging = true; } }); document.addEventListener('mouseup', function() { isDragging = false; setTimeout(() => { if (notesIcon.isDragging) { notesIcon.isDragging = false; } }, 10); }); document.body.appendChild(notesIcon); } app.registerExtension({ name: "NodeNotes", async beforeRegisterNodeDef(nodeType, nodeData) { // Store the original methods to call later const origOnNodeCreated = nodeType.prototype.onNodeCreated; const origGetComputeSize = nodeType.prototype.getComputeSize; const origOnDrawForeground = nodeType.prototype.onDrawForeground; const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; const origOnSerialize = nodeType.prototype.onSerialize; const origOnConfigure = nodeType.prototype.onConfigure; const origCollapse = nodeType.prototype.collapse; const origOnResize = nodeType.prototype.onResize; // Add notes array to the node nodeType.prototype.notes = []; // Override the onNodeCreated function nodeType.prototype.onNodeCreated = function() { if (origOnNodeCreated) { origOnNodeCreated.apply(this, arguments); } // Initialize note flags if (!this.flags) { this.flags = {}; } // Initialize empty notes array this.notes = []; }; // Override onSerialize to save notes with the node nodeType.prototype.onSerialize = function(o) { if (origOnSerialize) { origOnSerialize.apply(this, arguments); } // Save notes in the node's serialized data if (this.notes && this.notes.length > 0) { o.notes = this.notes; } }; // Override onConfigure to load notes when a node is configured nodeType.prototype.onConfigure = function(o) { if (origOnConfigure) { origOnConfigure.apply(this, arguments); } // Load notes from the node's configuration data if (o.notes && Array.isArray(o.notes)) { this.notes = o.notes; // Update global notes storage const noteId = this.id.toString(); nodeNotes[noteId] = [...this.notes]; // Mark node as having notes if (this.notes.length > 0) { this.flags.hasNotes = true; app.graph.setDirtyCanvas(true); } } }; // Override getExtraMenuOptions to add our menu items nodeType.prototype.getExtraMenuOptions = function(_, options) { // Call original method if it exists if (origGetExtraMenuOptions) { origGetExtraMenuOptions.apply(this, arguments); } // Add a separator options.push(null); // Check if node has notes if (this.notes && this.notes.length > 0) { // Add view notes entry options.push({ content: `📑 View Notes (${this.notes.length})`, callback: () => { openNotesModal(this); } }); } else { // Add add note entry options.push({ content: "📝 Add Note", callback: () => { openNotesModal(this); } }); } return options; }; // Override getComputeSize to make room for the note icon nodeType.prototype.getComputeSize = function() { let size = origGetComputeSize ? origGetComputeSize.apply(this, arguments) : [200, 100]; // Only add extra space if we have notes if (this.flags && this.flags.hasNotes) { size[0] += 20; // Add a bit of width for our icon } return size; }; // Override onDrawForeground to draw the note icon nodeType.prototype.onDrawForeground = function(ctx) { if (origOnDrawForeground) { origOnDrawForeground.apply(this, arguments); } if (this.flags && this.flags.hasNotes && this.notes && this.notes.length > 0 && !this.flags.collapsed) { ctx.save(); const noteIconSize = 16; const x = this.size[0] - noteIconSize - 5; const y = -noteIconSize - 4; // Draw a small info circle ctx.fillStyle = "#FF9800"; ctx.beginPath(); ctx.arc(x + noteIconSize/2, y + noteIconSize/2, noteIconSize/2, 0, Math.PI * 2); ctx.fill(); // Draw a number indicating how many notes ctx.fillStyle = "white"; ctx.font = "bold 10px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const noteCount = Math.min(this.notes.length, 9); const noteCountText = this.notes.length > 9 ? "9+" : noteCount.toString(); ctx.fillText(noteCountText, x + noteIconSize/2, y + noteIconSize/2); ctx.restore(); } }; // Override the collapse method to update canvas when collapsing nodeType.prototype.collapse = function(collapse) { if (origCollapse) { origCollapse.apply(this, arguments); } else { if (collapse === undefined) { collapse = !this.flags.collapsed; } this.flags.collapsed = collapse; } if (this.graph) { this.graph.setDirtyCanvas(true, true); } }; // Override the onResize method to update icon position when resizing nodeType.prototype.onResize = function(size) { if (origOnResize) { origOnResize.apply(this, arguments); } if (this.graph && this.flags && this.flags.hasNotes) { this.graph.setDirtyCanvas(true, true); } }; }, async setup() { // Create the UI early createNotesUI(); // Create the draggable notes icon createNotesIcon(); // Hook into the graph serialization process to save notes const origGraphToJSON = LiteGraph.LGraph.prototype.toJSON; LiteGraph.LGraph.prototype.toJSON = function() { const graphData = origGraphToJSON.apply(this, arguments); // Make sure there's an 'extra' field to store our data if (!graphData.extra) { graphData.extra = {}; } graphData.extra.nodeNotes = JSON.parse(JSON.stringify(nodeNotes)); return graphData; }; // Override graph load to restore notes const origLoadGraphData = app.loadGraphData; app.loadGraphData = function(graphData) { const result = origLoadGraphData.apply(this, arguments); // Load notes from graph extra data if (graphData && graphData.extra && graphData.extra.nodeNotes) { Object.assign(nodeNotes, JSON.parse(JSON.stringify(graphData.extra.nodeNotes))); } return result; }; // Explicitly register the extension with ComfyUI workflow serialization if (app.registerExtension) { app.registerExtension({ name: "NodeNotes.Serialization", beforeRegisterNodeDef(nodeType) { // If the node already has our extension loaded, don't add it again if (nodeType.prototype.hasOwnProperty("notes")) { return; } // Initialize empty notes array nodeType.prototype.notes = []; }, // Add properties to save in workflow getNodeProperties(node) { // If the node has notes, add them to saved properties if (node.notes && node.notes.length > 0) { return { notes: node.notes }; } return {}; }, // When loading properties from workflow loadedGraphNode(node, properties) { // If there are notes in the saved properties, restore them if (properties.notes && Array.isArray(properties.notes)) { node.notes = properties.notes; // Store in global collection const noteId = node.id.toString(); nodeNotes[noteId] = [...node.notes]; // Show note icon if there are notes if (node.notes.length > 0) { if (!node.flags) node.flags = {}; node.flags.hasNotes = true; } } } }); } console.log("Node Notes extension loaded"); } }); // Create UI for the global notes panel function createGlobalNotesPanel() { if (!document.getElementById('global-notes-panel-container')) { const panelContainer = document.createElement('div'); panelContainer.id = 'global-notes-panel-container'; panelContainer.className = 'global-notes-panel-container'; panelContainer.style.display = 'none'; // Panel content panelContainer.innerHTML = `
`; document.body.appendChild(panelContainer); // Add styles const style = document.createElement('style'); style.textContent = ` .global-notes-panel-container { position: fixed; top: 0; right: 0; width: 350px; height: 100%; background-color: rgba(0, 0, 0, 0.3); z-index: 9000; display: flex; justify-content: flex-end; } .global-notes-panel { width: 100%; height: 100%; background-color: #222; border-left: 1px solid #444; display: flex; flex-direction: column; } .global-notes-panel-header { padding: 15px; background-color: #333; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center; } .global-notes-panel-controls { display: flex; width: 100%; justify-content: space-between; align-items: center; } #global-notes-search { padding: 5px 10px; background-color: #444; border: 1px solid #555; border-radius: 4px; color: white; flex-grow: 1; margin-right: 10px; } #global-notes-refresh-btn { background: none; border: none; font-size: 18px; cursor: pointer; color: #aaa; display: flex; align-items: center; justify-content: center; margin-right: 10px; } #global-notes-refresh-btn:hover { color: #FF9800; } .global-notes-panel-footer { padding: 15px; background-color: #333; border-top: 1px solid #444; display: flex; gap: 10px; } #global-notes-import-all-btn, #global-notes-export-btn { padding: 8px 16px; background-color: #FF9800; color: #111; border: none; border-radius: 4px; cursor: pointer; flex-grow: 1; } #global-notes-import-all-btn:hover, #global-notes-export-btn:hover { background-color: #FFA726; } #global-notes-close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #aaa; display: flex; align-items: center; justify-content: center; } #global-notes-close-btn:hover { color: #FF9800; } .global-notes-panel-body { flex-grow: 1; overflow-y: auto; padding: 15px; } .global-node-notes-group { margin-bottom: 20px; border: 1px solid #444; border-radius: 4px; overflow: hidden; } .global-node-header { background-color: #333; padding: 10px 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; } .global-node-header.collapsed .collapse-indicator { transform: rotate(-90deg); } .collapse-indicator { display: inline-block; margin-right: 5px; transition: transform 0.2s ease; color: #FF9800; font-size: 10px; cursor: pointer; } .global-node-title-text { cursor: pointer; } .global-node-notes-count { cursor: pointer; } .global-node-notes { padding: 10px 15px; max-height: 300px; overflow-y: auto; } .global-note-item { padding: 8px 0; border-bottom: 1px solid #444; } .global-note-item:last-child { border-bottom: none; } .global-note-header { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 12px; color: #aaa; } .global-note-content { color: white; font-size: 14px; white-space: pre-wrap; max-height: 100px; overflow-y: auto; } .global-note-actions { display: flex; gap: 5px; margin-top: 5px; justify-content: flex-end; } .global-note-btn { padding: 3px 8px; background-color: #444; border: none; border-radius: 3px; color: white; cursor: pointer; font-size: 12px; } .global-note-btn:hover { background-color: #555; } .no-notes-message { color: #aaa; text-align: center; padding: 20px; } `; document.head.appendChild(style); // Setup event listeners setupGlobalNotesPanelEventListeners(); } } // Setup event listeners for global notes panel function setupGlobalNotesPanelEventListeners() { const panelContainer = document.getElementById('global-notes-panel-container'); // Close button document.getElementById('global-notes-close-btn').addEventListener('click', function() { panelContainer.style.display = 'none'; }); // Refresh button document.getElementById('global-notes-refresh-btn').addEventListener('click', function() { refreshGlobalNotes(); }); // Export button document.getElementById('global-notes-export-btn').addEventListener('click', function() { exportAllNotes(); }); // Import button document.getElementById('global-notes-import-all-btn').addEventListener('click', function() { importAllNotes(); }); // Search functionality document.getElementById('global-notes-search').addEventListener('input', function() { const searchTerm = this.value.toLowerCase(); filterGlobalNotes(searchTerm); }); } // Filter global notes based on search term function filterGlobalNotes(searchTerm) { const nodeGroups = document.querySelectorAll('.global-node-notes-group'); nodeGroups.forEach(group => { const notes = group.querySelectorAll('.global-note-item'); let hasVisibleNotes = false; notes.forEach(note => { const content = note.querySelector('.global-note-content').textContent.toLowerCase(); const date = note.querySelector('.global-note-date').textContent.toLowerCase(); if (content.includes(searchTerm) || date.includes(searchTerm)) { note.style.display = 'block'; hasVisibleNotes = true; } else { note.style.display = 'none'; } }); // Show or hide the node group based on whether it has any visible notes group.style.display = hasVisibleNotes ? 'block' : 'none'; }); } // Render all notes for a node in global panel function renderGlobalNotes() { const notesList = document.getElementById('global-notes-list'); notesList.innerHTML = ''; // Check if we have any notes const hasNotes = Object.keys(nodeNotes).some(nodeId => Array.isArray(nodeNotes[nodeId]) && nodeNotes[nodeId].length > 0 ); if (!hasNotes) { notesList.innerHTML = '
No notes found in this workflow
'; return; } // Group notes by node for (const nodeId in nodeNotes) { if (Array.isArray(nodeNotes[nodeId]) && nodeNotes[nodeId].length > 0) { const node = app.graph._nodes_by_id[parseInt(nodeId)]; const nodeTitle = node ? (node.title || `Node #${nodeId}`) : `Node #${nodeId}`; const nodeGroup = document.createElement('div'); nodeGroup.className = 'global-node-notes-group'; nodeGroup.dataset.nodeId = nodeId; // Check if this node group was previously collapsed const storageKey = `node_notes_collapsed_${nodeId}`; const isCollapsed = localStorage.getItem(storageKey) === 'true'; const collapsedClass = isCollapsed ? 'collapsed' : ''; nodeGroup.innerHTML = `
▼ ${escapeHTML(nodeTitle)} ${nodeNotes[nodeId].length}
${nodeNotes[nodeId].map((note, index) => `
${note.date}
${escapeHTML(note.text)}
`).join('')}
`; notesList.appendChild(nodeGroup); } } // Add event listeners to the node headers to focus the node and toggle collapse notesList.querySelectorAll('.global-node-header').forEach(header => { header.addEventListener('click', function(e) { const nodeId = parseInt(this.parentElement.dataset.nodeId); const node = app.graph._nodes_by_id[nodeId]; if (e.target.closest('.collapse-indicator')) { const nodeGroup = this.parentElement; const notesContainer = nodeGroup.querySelector('.global-node-notes'); const isCollapsed = this.classList.toggle('collapsed'); const storageKey = `node_notes_collapsed_${nodeId}`; localStorage.setItem(storageKey, isCollapsed); if (isCollapsed) { notesContainer.style.display = 'none'; } else { notesContainer.style.display = 'block'; } e.stopPropagation(); } else if (e.target.closest('.global-node-title-text') || e.target.closest('.global-node-notes-count')) { if (node) { app.canvas.centerOnNode(node); } } }); }); // Add event listeners for edit/delete buttons notesList.querySelectorAll('.global-edit-note').forEach(button => { button.addEventListener('click', function(e) { e.stopPropagation(); const nodeId = parseInt(this.dataset.nodeId); const noteIndex = parseInt(this.dataset.noteIndex); const node = app.graph._nodes_by_id[nodeId]; if (node) { // Open the notes modal in edit mode for this specific note openNotesModal(node); setTimeout(() => { editExistingNote(noteIndex); }, 100); } }); }); notesList.querySelectorAll('.global-delete-note').forEach(button => { button.addEventListener('click', function(e) { e.stopPropagation(); const nodeId = parseInt(this.dataset.nodeId); const noteIndex = parseInt(this.dataset.noteIndex); const node = app.graph._nodes_by_id[nodeId]; if (node && confirm("Delete this note?")) { if (node.notes && noteIndex < node.notes.length) { node.notes.splice(noteIndex, 1); // Update global notes storage nodeNotes[nodeId] = [...node.notes]; if (node.notes.length === 0) { node.flags.hasNotes = false; } // Mark graph as dirty to ensure it gets saved if (app.graph) { app.graph.change(); } // Refresh the global notes panel renderGlobalNotes(); } } }); }); } // Open global notes panel and populate it with all notes function openGlobalNotesPanel() { createGlobalNotesPanel(); const panelContainer = document.getElementById('global-notes-panel-container'); panelContainer.style.display = 'flex'; renderGlobalNotes(); } // Function to update the notes list in the panel function refreshGlobalNotes() { Object.keys(nodeNotes).forEach(key => delete nodeNotes[key]); const nodes = app.graph._nodes; if (nodes && nodes.length > 0) { nodes.forEach(node => { if (node.notes && node.notes.length > 0) { nodeNotes[node.id.toString()] = [...node.notes]; } }); } if (document.getElementById('global-notes-panel-container').style.display === 'flex') { renderGlobalNotes(); } } // Export all notes function exportAllNotes() { const notesToExport = {}; for (const nodeId in nodeNotes) { if (Array.isArray(nodeNotes[nodeId]) && nodeNotes[nodeId].length > 0) { notesToExport[nodeId] = [...nodeNotes[nodeId]]; } } notesToExport._nodeTitles = {}; app.graph._nodes.forEach(node => { if (node.title) { notesToExport._nodeTitles[node.id.toString()] = node.title; } }); if (Object.keys(notesToExport).length <= 1) { // if only _nodeTitles exist alert("No notes to export."); return; } const fileName = "node_notes.json"; const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(notesToExport, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", fileName); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); } // Temporarily highlight a node function highlightNode(node) { app.canvas.centerOnNode(node); } // Toggle global notes panel function toggleGlobalNotesPanel() { const panelContainer = document.getElementById('global-notes-panel-container'); if (panelContainer && panelContainer.style.display === 'flex') { panelContainer.style.display = 'none'; } else { openGlobalNotesPanel(); } } // Import notes through the panel function importAllNotes() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.onchange = function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { try { const importData = JSON.parse(event.target.result); if (typeof importData === 'object' && importData !== null) { const importedNodeTitles = {}; if (importData._nodeTitles) { Object.assign(importedNodeTitles, importData._nodeTitles); delete importData._nodeTitles; } const validNodes = Object.keys(importData).filter(nodeId => Array.isArray(importData[nodeId]) && importData[nodeId].length > 0 ); if (validNodes.length === 0) { alert("No notes to import in the selected file."); return; } const importMapping = prepareImportMapping(importData, importedNodeTitles); showImportMappingDialog(importMapping); } else { throw new Error("Invalid import format. Expected an object with node IDs and note arrays."); } } catch (error) { console.error("Error importing notes:", error); alert("Error importing notes: " + error.message); } }; reader.readAsText(file); }; fileInput.click(); } // Prepare data for import dialog function prepareImportMapping(importData, importedNodeTitles) { const existingNodes = {}; const existingNodesByTitle = {}; app.graph._nodes.forEach(node => { const nodeId = node.id.toString(); const nodeTitle = node.title || `Node #${nodeId}`; const nodeType = node.type; existingNodes[nodeId] = { id: nodeId, title: nodeTitle, type: nodeType, node: node }; if (node.title) { if (!existingNodesByTitle[node.title]) { existingNodesByTitle[node.title] = []; } existingNodesByTitle[node.title].push(existingNodes[nodeId]); } }); const importMapping = []; for (const sourceNodeId in importData) { const notes = importData[sourceNodeId]; if (!Array.isArray(notes) || notes.length === 0) continue; let sourceNodeTitle = ''; if (importedNodeTitles && importedNodeTitles[sourceNodeId]) { sourceNodeTitle = importedNodeTitles[sourceNodeId]; } else if (notes[0] && notes[0].nodeTitle) { sourceNodeTitle = notes[0].nodeTitle; } let suggestedTarget = null; let matchReason = ''; // Check for matching options if (existingNodes[sourceNodeId] && (!sourceNodeTitle || existingNodes[sourceNodeId].title === sourceNodeTitle)) { suggestedTarget = existingNodes[sourceNodeId]; matchReason = 'Full match (ID and title)'; } else if (existingNodes[sourceNodeId]) { suggestedTarget = existingNodes[sourceNodeId]; matchReason = 'ID match'; } else if (sourceNodeTitle && existingNodesByTitle[sourceNodeTitle]) { if (existingNodesByTitle[sourceNodeTitle].length === 1) { suggestedTarget = existingNodesByTitle[sourceNodeTitle][0]; matchReason = 'Title match'; } } importMapping.push({ sourceNodeId: sourceNodeId, sourceNodeTitle: sourceNodeTitle || `Node #${sourceNodeId}`, notes: notes, notesCount: notes.length, previewText: notes[0].text.substring(0, 100) + (notes[0].text.length > 100 ? '...' : ''), suggestedTarget: suggestedTarget, matchReason: matchReason, targetNode: suggestedTarget ? suggestedTarget.node : null }); } return { mappings: importMapping, existingNodes: existingNodes, existingNodesByTitle: existingNodesByTitle }; } // Show dialog for selecting target nodes for all notes function showImportMappingDialog(importMapping) { const modalContainer = document.createElement('div'); modalContainer.id = 'notes-import-mapping-modal'; modalContainer.className = 'notes-modal-container'; modalContainer.style.display = 'flex'; const existingNodes = Object.values(importMapping.existingNodes); const nodesByType = {}; existingNodes.forEach(node => { if (!nodesByType[node.type]) { nodesByType[node.type] = []; } nodesByType[node.type].push(node); }); const sortedTypes = Object.keys(nodesByType).sort(); sortedTypes.forEach(type => { nodesByType[type].sort((a, b) => a.title.localeCompare(b.title)); }); const allNodes = existingNodes.map(node => ({ id: node.id, title: node.title, type: node.type, searchText: `${node.title.toLowerCase()} ${node.type.toLowerCase()} ${node.id}` })); modalContainer.innerHTML = `

Select Notes to Import

Review and confirm note assignments for each node:

${importMapping.mappings.map((mapping, index) => { const isAutoMatched = mapping.suggestedTarget !== null; const matchClass = isAutoMatched ? 'has-match' : 'no-match'; const matchIcon = isAutoMatched ? '✓' : '❓'; return `
${mapping.sourceNodeTitle} (${mapping.notesCount} notes)
${escapeHTML(mapping.previewText)}
${matchIcon} →
${isAutoMatched ? `
${mapping.matchReason}
` : '
Node selection required
'}
`; }).join('')}
`; document.body.appendChild(modalContainer); // Add styles const style = document.createElement('style'); style.textContent = ` #import-mappings-container { max-height: 60vh; overflow-y: auto; } .import-instructions { margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; } .import-select-all-container { display: flex; align-items: center; gap: 15px; } .small-btn { padding: 4px 8px; background-color: #555; border: none; border-radius: 3px; color: white; cursor: pointer; font-size: 12px; } .small-btn:hover { background-color: #666; } .import-mapping-item { margin-bottom: 15px; padding: 12px; border: 1px solid #444; border-radius: 4px; background-color: #333; display: flex; gap: 10px; } .import-mapping-item.has-match { border-left: 3px solid #4CAF50; } .import-mapping-item.no-match { border-left: 3px solid #FFC107; } .import-mapping-checkbox { display: flex; align-items: center; padding-top: 10px; } .import-mapping-content { flex-grow: 1; display: flex; align-items: center; gap: 10px; } .import-mapping-source { flex: 1; } .import-mapping-arrow { display: flex; flex-direction: column; align-items: center; padding: 0 5px; } .match-icon { font-size: 14px; margin-bottom: 5px; } .arrow { font-size: 18px; color: #FF9800; } .import-mapping-target { flex: 1; } .import-mapping-preview { color: #aaa; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 5px; } .select-with-search { position: relative; margin-bottom: 5px; } .search-container { margin-bottom: 5px; } .node-search-input { width: 100%; padding: 6px; background-color: #333; border: 1px solid #555; border-radius: 4px; color: white; font-size: 13px; } .target-node-select { width: 100%; padding: 8px; background-color: #444; border: 1px solid #555; border-radius: 4px; color: white; } .match-reason { font-size: 12px; color: #4CAF50; } .match-reason.no-match { color: #FFC107; } .notes-count { color: #FF9800; font-size: 13px; margin-left: 5px; } .option-highlighted { background-color: #FF9800; color: #000; } `; document.head.appendChild(style); // Event listeners document.querySelector('#notes-import-mapping-modal .notes-modal-close').addEventListener('click', function() { modalContainer.remove(); }); document.getElementById('cancel-import-mapping-btn').addEventListener('click', function() { modalContainer.remove(); }); const selectAllCheckbox = document.getElementById('select-all-checkbox'); selectAllCheckbox.addEventListener('change', function() { const isChecked = this.checked; document.querySelectorAll('.mapping-checkbox').forEach(checkbox => { checkbox.checked = isChecked; }); updateImportButtonLabel(); }); document.getElementById('apply-suggested-btn').addEventListener('click', function() { importMapping.mappings.forEach((mapping, index) => { const select = document.querySelector(`.target-node-select[data-index="${index}"]`); const checkbox = document.querySelector(`.mapping-checkbox[data-index="${index}"]`); if (mapping.suggestedTarget) { select.value = mapping.suggestedTarget.id; checkbox.checked = true; const mappingItem = document.querySelector(`.import-mapping-item[data-index="${index}"]`); mappingItem.classList.add('has-match'); mappingItem.classList.remove('no-match'); const matchIcon = mappingItem.querySelector('.match-icon'); matchIcon.textContent = '✓'; matchIcon.title = mapping.matchReason; const matchReason = mappingItem.querySelector('.match-reason'); matchReason.textContent = mapping.matchReason; matchReason.classList.remove('no-match'); } }); updateImportButtonLabel(); }); // Search functionality document.querySelectorAll('.node-search-input').forEach(input => { input.addEventListener('input', function() { const index = this.dataset.index; const select = document.querySelector(`.target-node-select[data-index="${index}"]`); const searchTerm = this.value.toLowerCase().trim(); if (searchTerm === '') { resetSelectOptions(select); } else { filterSelectOptions(select, searchTerm, allNodes); } }); }); function resetSelectOptions(select) { const optgroups = select.querySelectorAll('optgroup'); optgroups.forEach(group => { group.style.display = ''; const options = group.querySelectorAll('option'); options.forEach(option => { option.style.display = ''; option.classList.remove('option-highlighted'); }); }); } function filterSelectOptions(select, searchTerm, allNodes) { const optgroups = select.querySelectorAll('optgroup'); // Hide all options first optgroups.forEach(group => { group.style.display = 'none'; const options = group.querySelectorAll('option'); options.forEach(option => { option.style.display = 'none'; option.classList.remove('option-highlighted'); }); }); // Find matching nodes const matchingNodes = allNodes.filter(node => node.searchText.includes(searchTerm) ); // Show matching options matchingNodes.forEach(node => { const option = select.querySelector(`option[value="${node.id}"]`); if (option) { option.style.display = ''; if (node.title.toLowerCase().includes(searchTerm)) { option.classList.add('option-highlighted'); } const group = option.closest('optgroup'); if (group) { group.style.display = ''; } } }); // Auto-select single match if (matchingNodes.length === 1) { select.value = matchingNodes[0].id; const event = new Event('change'); select.dispatchEvent(event); } } // Change handlers for dropdowns document.querySelectorAll('.target-node-select').forEach(select => { select.addEventListener('change', function() { const index = this.dataset.index; const mappingItem = document.querySelector(`.import-mapping-item[data-index="${index}"]`); const checkbox = document.querySelector(`.mapping-checkbox[data-index="${index}"]`); const matchIcon = mappingItem.querySelector('.match-icon'); const matchReason = mappingItem.querySelector('.match-reason'); if (this.value) { mappingItem.classList.add('has-match'); mappingItem.classList.remove('no-match'); matchIcon.textContent = '✓'; matchIcon.title = 'Selected manually'; matchReason.textContent = 'Selected manually'; matchReason.classList.remove('no-match'); checkbox.checked = true; } else { mappingItem.classList.remove('has-match'); mappingItem.classList.add('no-match'); matchIcon.textContent = '❓'; matchIcon.title = 'No automatic match'; matchReason.textContent = 'Node selection required'; matchReason.classList.add('no-match'); checkbox.checked = false; } updateImportButtonLabel(); }); }); // Checkbox handlers document.querySelectorAll('.mapping-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { updateImportButtonLabel(); const allChecked = Array.from(document.querySelectorAll('.mapping-checkbox')).every(cb => cb.checked); selectAllCheckbox.checked = allChecked; }); }); function updateImportButtonLabel() { const checkedCount = document.querySelectorAll('.mapping-checkbox:checked').length; const importButton = document.getElementById('import-selected-mappings-btn'); importButton.textContent = `Import Selected (${checkedCount})`; importButton.disabled = checkedCount === 0; } updateImportButtonLabel(); // Import button action document.getElementById('import-selected-mappings-btn').addEventListener('click', function() { const selectedMappings = []; document.querySelectorAll('.mapping-checkbox:checked').forEach(checkbox => { const index = parseInt(checkbox.dataset.index); const select = document.querySelector(`.target-node-select[data-index="${index}"]`); const targetNodeId = select.value; if (targetNodeId && index < importMapping.mappings.length) { selectedMappings.push({ mapping: importMapping.mappings[index], targetNodeId: targetNodeId }); } }); if (selectedMappings.length > 0) { let importCount = 0; selectedMappings.forEach(item => { const targetNodeId = item.targetNodeId; const notes = item.mapping.notes; const targetNode = importMapping.existingNodes[targetNodeId].node; if (targetNode) { targetNode.notes = targetNode.notes || []; const notesToImport = notes.filter(note => typeof note === 'object' && note !== null && note.text); if (targetNode.title) { notesToImport.forEach(note => { note.nodeTitle = targetNode.title; }); } targetNode.notes = [...targetNode.notes, ...notesToImport]; nodeNotes[targetNodeId] = [...targetNode.notes]; targetNode.flags = targetNode.flags || {}; targetNode.flags.hasNotes = true; importCount++; } }); modalContainer.remove(); app.graph.setDirtyCanvas(true); const totalImportCount = importCount; if (totalImportCount > 0) { alert(`Successfully imported notes for ${totalImportCount} nodes.`); openGlobalNotesPanel(); if (app.graph) { app.graph.change(); } } else { alert("No notes were imported."); } } else { alert("No nodes selected for importing notes."); } }); }