diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ac9510f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+# ComfyUI Note Manager
+A powerful extension for ComfyUI that enables adding notes to many node in your workflow. Keep track of important settings, reminders, and workflow documentation directly within your ComfyUI canvas.
+
+## Features
+- 📝 **Add/Edit/View Notes:** Easily add, modify, and view multiple timestamped notes for many node.
+- 🔢 **Note Count Indicator:** Shows a clear icon with the note count on nodes that contain notes (when not collapsed).
+- 💾 **Auto-Saves with Workflow:** Notes are saved directly within your workflow JSON file.
+- 🎨 **Modern UI:** Clean modal windows for managing notes per node.
+- 📤 **Node-Specific Import/Export:** Share or back up notes for individual nodes using JSON format.
+- 🔍 **Selective Export:** Choose specific notes to include when exporting from a node.
+- 📋 **Global Notes Panel:** View, search, and manage notes from *all* nodes in a dedicated, toggleable side panel.
+- 🖱️ **Draggable Panel Toggle:** A floating `📋` icon lets you toggle the Global Notes Panel and can be dragged anywhere on the screen.
+- ✈️ **Jump to Node:** Quickly navigate to a node in your workflow by clicking its title in the Global Notes Panel.
+- 🌍 **Global Import/Export:** Manage notes for the *entire* workflow, including an intelligent import mapping feature.
+- 🧩 **Broad Compatibility:** Designed to work with many types of ComfyUI nodes.
+## Installation
+
+Manually:
+
+Clone this repo to ComfyUI/custom_nodes
+
+## Usage
+
+### Basic Note Management
+
+1. **Add a note to a node**:
+ * Right-click on any node and select "📝 Add Note" from the context menu.
+ * Enter your note in the modal window and click "Save".
+2. **View notes on a node**:
+ * Right-click on a node with notes and select "📑 View Notes (`N`)" from the context menu (where `N` is the note count).
+3. **Edit or delete notes**:
+ * In the notes modal, use the ✏️ button to edit a note.
+ * Use the 🗑️ button to delete a note.
+4. **Identify nodes with notes**:
+ * Nodes with notes display an orange circle with the number of notes above the title (when not collapsed).
+
+### Global Notes Panel
+
+1. **Access the global notes panel**:
+ * Click the draggable `📋` icon on the screen to toggle the panel.
+ * All notes from your workflow are displayed, organized by node.
+2. **Quick navigation**:
+ * Click on a node name in the global panel to center the canvas on that node.
+3. **Manage & Search**:
+ * Use the search bar to filter notes across the entire workflow.
+ * Edit/Delete notes directly from the panel list.
+ * Use the global Import/Export buttons for workflow-wide note management.
+
+### Import and Export
+
+1. **Export notes**:
+ * **Node Specific:** Open the notes modal for a node, click "Import/Export", choose "Export Node Notes", select notes via checkboxes, and click "Export Selected".
+ * **Global:** Open the Global Notes Panel and click the "Export" button
+2. **Import notes**:
+ * **Node Specific:** Open the notes modal for the target node, click "Import/Export", choose "Import Node Notes", and select the JSON file.
+ * **Global:** Open the Global Notes Panel, click the "Import" button, select the JSON file, and use the mapping dialog to assign notes to the correct nodes before confirming.
+
+## Tips and Tricks
+- **Updating notes**: When adding or editing notes, or when you open a new workflow tab, you need to refresh the manager using the 🔄 button next to the note search field; otherwise, you may experience a synchronization issue.
+- **Workflow Documentation**: Use notes on key nodes (like Loaders, Checkpoints, Samplers) to explain their role or settings.
+- **Parameter Tracking**: Note down specific seeds, CFG scales, or prompt keywords that produced interesting results.
+- **Troubleshooting**: Document errors encountered or specific settings needed to make a custom node work correctly.
+- **Idea Repository**: Jot down ideas for workflow improvements or variations directly on the relevant nodes.
+- **Global Overview**: Use the global panel to quickly review all annotations in a complex workflow.
+
+## Screenshots
+
+
+### Context menu
+
+
+### Note Modal Window
+
+
+### Global Import Mapping Dialog
+
+Here are the 5 options:
+- 🔴 Node selection required – You must manually select the node to which the note should be added.
+- 🟡 Title match – Matches the node's name, but not its ID.
+- 🟡 ID match – Use this if you renamed the node to which you want to attach the note and are absolutely sure of your actions.
+- 🟢 Full match (ID and title) – The best option for loading; it ensures a 100% match.
+- ⚪ Selected manually – The node is selected manually.
+
+### Export Selection
+
+
+
+
+
+
+
+
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..f36bb63
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,5 @@
+WEB_DIRECTORY = "js"
+NODE_CLASS_MAPPINGS = {}
+__all__ = ["NODE_CLASS_MAPPINGS"]
+
+print("ComfyUI Node Notes extension loaded.")
\ No newline at end of file
diff --git a/js/node_notes.js b/js/node_notes.js
new file mode 100644
index 0000000..692360c
--- /dev/null
+++ b/js/node_notes.js
@@ -0,0 +1,2368 @@
+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 = `
+
+ `;
+
+ 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 = `
+
+ ${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 = `
+
+
+
+
+ ${notesToExport.map((nodeData, nodeIdx) => `
+
+
+
+ ${nodeData.notes.map((note, noteIdx) => `
+
+
+
+ ${note.date} - ${escapeHTML(note.text.length > 50 ? note.text.substring(0, 50) + '...' : note.text)}
+
+
+ `).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 = `
+
+
+ ${nodeNotes[nodeId].map((note, index) => `
+
+
+
${escapeHTML(note.text)}
+
+ Edit
+ Delete
+
+
+ `).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 = `
+
+
+
+
+
Review and confirm note assignments for each node:
+
+ Select All
+ Apply Suggestions
+
+
+
+ ${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}
+ →
+
+
+
+
+
+
+
+ -- Select node --
+ ${sortedTypes.map(type => `
+
+ ${nodesByType[type].map(node => {
+ const selected = mapping.suggestedTarget && mapping.suggestedTarget.id === node.id ? 'selected' : '';
+ return `${node.title} `;
+ }).join('')}
+
+ `).join('')}
+
+
+ ${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.");
+ }
+ });
+}
+
\ No newline at end of file