import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; class AutoNotesManager { constructor() { this.sidebar = null; // Load last display mode from localStorage, default to "all" this.displayMode = localStorage.getItem('autonotes_displayMode') || "all"; this.notes = []; this.folders = []; this.editDialog = null; this.selectedNodeType = null; this.selectedNodeAttributes = {}; this.workflowName = ""; this.nodeTypes = {}; // Store node types and their definitions this.expandedFolders = new Set(); // Track which folders are expanded this.lastSelectedNodeType = null; // Track last selected node to detect changes this.init(); } async init() { // Wait for app to be ready await new Promise(resolve => { if (app.canvas) { resolve(); } else { app.registerExtension({ name: "AutoNotes.Init", setup() { resolve(); } }); } }); // Load marked library await this.loadMarkedLibrary(); // Load node definitions await this.loadNodeTypes(); this.createSidebar(); this.setupEventListeners(); await this.refreshNotes(); } async loadMarkedLibrary() { // Load the marked library using script tag since it's UMD format if (typeof window.marked !== 'undefined') { this.marked = window.marked; return; } return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = '/extensions/ComfyUI-AutoNotes/marked.min.js'; script.onload = () => { this.marked = window.marked; resolve(); }; script.onerror = () => { console.error('Failed to load marked library'); this.marked = null; resolve(); // Resolve anyway, will use fallback }; document.head.appendChild(script); }); } async loadNodeTypes() { try { const response = await api.fetchApi('/object_info'); this.nodeTypes = await response.json(); } catch (error) { console.error('Failed to load node types:', error); this.nodeTypes = {}; } } createSidebar() { // Load saved width from localStorage const savedWidth = localStorage.getItem('autonotes_sidebarWidth') || '300'; // Calculate top bar height dynamically const calculateTopBarHeight = () => { // Check if user has manually set a value in localStorage const manualHeight = localStorage.getItem('autonotes_topBarHeight'); if (manualHeight) { const height = parseInt(manualHeight); console.log('AutoNotes: Using manually configured top bar height:', height); return height; } // Look for ComfyUI's main menu bar that contains buttons like Manager, Queue, etc. const allElements = document.querySelectorAll('*'); let candidates = []; for (const el of allElements) { const style = window.getComputedStyle(el); const position = style.position; if (position === 'fixed' || position === 'absolute') { const rect = el.getBoundingClientRect(); // Look for elements near the top that are reasonably wide (likely to be top bars) // Relaxed criteria: width > 300 instead of 500 if (rect.top >= 0 && rect.top <= 100 && rect.width > 300 && rect.bottom < 250 && rect.bottom > 50) { candidates.push({ bottom: rect.bottom, width: rect.width, element: el.tagName }); } } } // Sort by bottom position if (candidates.length > 0) { candidates.sort((a, b) => b.bottom - a.bottom); const topBarBottom = Math.ceil(candidates[0].bottom); console.log('AutoNotes: Detected top bar height:', topBarBottom, 'from', candidates[0].element, 'candidates:', candidates.length); console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")'); return topBarBottom; } // Fallback - use a reasonable default that's close to the top bar console.log('AutoNotes: Using fallback top bar height: 140 (no candidates found)'); console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")'); return 140; }; const topBarHeight = calculateTopBarHeight(); // Create sidebar container - full height with padding at top this.sidebar = document.createElement('div'); this.sidebar.id = 'autonotes-sidebar'; this.sidebar.style.cssText = ` position: fixed; right: 0; top: 0; width: ${savedWidth}px; height: 100vh; background: #2a2a2a; border-left: 1px solid #555; z-index: 1000; display: flex; flex-direction: column; padding: ${topBarHeight}px 10px 10px 10px; box-sizing: border-box; font-family: Arial, sans-serif; color: #fff; `; // Create resize handle const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; left: 0; top: 0; width: 5px; height: 100%; cursor: ew-resize; background: transparent; z-index: 1001; `; resizeHandle.addEventListener('mouseenter', () => { resizeHandle.style.background = '#007acc'; }); resizeHandle.addEventListener('mouseleave', () => { if (!this.isResizing) { resizeHandle.style.background = 'transparent'; } }); // Resize functionality this.isResizing = false; let startX, startWidth; resizeHandle.addEventListener('mousedown', (e) => { this.isResizing = true; startX = e.clientX; startWidth = parseInt(this.sidebar.style.width); resizeHandle.style.background = '#007acc'; document.body.style.cursor = 'ew-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!this.isResizing) return; const dx = startX - e.clientX; const newWidth = Math.max(200, Math.min(800, startWidth + dx)); this.sidebar.style.width = newWidth + 'px'; }); document.addEventListener('mouseup', () => { if (this.isResizing) { this.isResizing = false; resizeHandle.style.background = 'transparent'; document.body.style.cursor = ''; document.body.style.userSelect = ''; // Save width to localStorage localStorage.setItem('autonotes_sidebarWidth', parseInt(this.sidebar.style.width)); } }); this.sidebar.appendChild(resizeHandle); // Control panel const controlPanel = document.createElement('div'); controlPanel.style.cssText = ` margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #555; `; // Display mode selector const modeContainer = document.createElement('div'); modeContainer.style.marginBottom = '10px'; const modeLabel = document.createElement('label'); modeLabel.textContent = 'Display: '; modeLabel.style.marginRight = '10px'; const modeSelect = document.createElement('select'); modeSelect.style.cssText = ` background: #333; color: #fff; border: 1px solid #555; padding: 5px; `; modeSelect.innerHTML = ` `; modeSelect.value = this.displayMode; modeSelect.addEventListener('change', (e) => { this.displayMode = e.target.value; localStorage.setItem('autonotes_displayMode', this.displayMode); this.refreshNotes(); }); modeContainer.appendChild(modeLabel); modeContainer.appendChild(modeSelect); // Buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; const addButton = document.createElement('button'); addButton.textContent = 'Add'; addButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; addButton.addEventListener('click', () => this.addNote()); const editButton = document.createElement('button'); editButton.textContent = 'Edit'; editButton.style.cssText = ` background: #666; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; editButton.addEventListener('click', () => this.openEditDialog()); buttonContainer.appendChild(addButton); buttonContainer.appendChild(editButton); controlPanel.appendChild(modeContainer); controlPanel.appendChild(buttonContainer); // Notes display area this.notesContainer = document.createElement('div'); this.notesContainer.style.cssText = ` flex: 1; overflow-y: auto; padding-right: 5px; `; this.sidebar.appendChild(controlPanel); this.sidebar.appendChild(this.notesContainer); document.body.appendChild(this.sidebar); } setupEventListeners() { // Use polling to check for selection changes (more reliable) this.lastSelectionCheck = null; setInterval(() => { if (app.canvas && app.canvas.selected_nodes) { const selectedNodeIds = Object.keys(app.canvas.selected_nodes); const currentSelection = selectedNodeIds.join(','); // Only update if selection changed if (currentSelection !== this.lastSelectionCheck) { this.lastSelectionCheck = currentSelection; this.updateSelectedNode(); } } }, 300); // Check every 300ms // Listen for workflow changes api.addEventListener("graphChanged", () => { this.updateWorkflowName(); }); } updateSelectedNode() { // Try multiple methods to get selected nodes let selectedNodes = app.canvas?.selected_nodes; // Alternative: check graph canvas if (!selectedNodes && app.graph?.list_of_graphcanvas?.[0]) { selectedNodes = app.graph.list_of_graphcanvas[0].selected_nodes; } let currentNodeType = null; // Check if it's an object with keys (not a Set) const selectedNodeIds = selectedNodes ? Object.keys(selectedNodes) : []; if (selectedNodeIds.length > 0) { // Get the first selected node const nodeId = selectedNodeIds[0]; const node = selectedNodes[nodeId]; if (node && node.type) { currentNodeType = node.type; this.selectedNodeType = node.type; // Collect node attributes (widgets/inputs) this.selectedNodeAttributes = {}; if (node.widgets) { for (const widget of node.widgets) { this.selectedNodeAttributes[widget.name] = widget.value; } } } } else { this.selectedNodeType = null; this.selectedNodeAttributes = {}; } // Only refresh if selection changed and in automatic mode if (this.displayMode === "automatic" && currentNodeType !== this.lastSelectedNodeType) { this.lastSelectedNodeType = currentNodeType; this.refreshNotes(); } } updateWorkflowName() { // Try to get workflow name from various sources this.workflowName = app.ui.lastQueueSize || document.title || ""; if (this.displayMode === "automatic") { this.refreshNotes(); } } async refreshNotes() { try { const params = new URLSearchParams({ mode: this.displayMode }); if (this.selectedNodeType) { params.append('node_type', this.selectedNodeType); } if (Object.keys(this.selectedNodeAttributes).length > 0) { params.append('node_attributes', JSON.stringify(this.selectedNodeAttributes)); } if (this.workflowName) { params.append('workflow_name', this.workflowName); } // Collect workflow node information for node_in_workflow and attribute_in_workflow triggers const workflowNodes = {}; if (app.graph && app.graph._nodes) { for (const node of app.graph._nodes) { // For each node type in workflow, collect first instance's attributes if (!workflowNodes[node.type]) { const attributes = {}; if (node.properties) { Object.assign(attributes, node.properties); } if (node.widgets) { for (const widget of node.widgets) { attributes[widget.name] = widget.value; } } workflowNodes[node.type] = attributes; } } } if (Object.keys(workflowNodes).length > 0) { params.append('workflow_nodes', JSON.stringify(workflowNodes)); } const response = await api.fetchApi(`/autonotes/notes?${params}`); this.notes = await response.json(); this.renderNotes(); } catch (error) { console.error('Failed to fetch notes:', error); } } renderNotes() { this.notesContainer.innerHTML = ''; if (this.notes.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.textContent = 'No notes to display'; emptyMessage.style.cssText = ` text-align: center; color: #888; font-style: italic; margin-top: 20px; `; this.notesContainer.appendChild(emptyMessage); return; } for (const note of this.notes) { this.renderNote(note); } } renderNote(note) { const noteElement = document.createElement('div'); noteElement.style.cssText = ` margin-bottom: 15px; border: 1px solid #555; border-radius: 4px; overflow: hidden; `; // Note header (collapsible) const header = document.createElement('div'); header.style.cssText = ` background: #333; padding: 10px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; `; const title = document.createElement('span'); title.textContent = note.name || 'Untitled Note'; title.style.fontWeight = 'bold'; const controls = document.createElement('div'); controls.style.display = 'flex'; controls.style.gap = '5px'; controls.style.alignItems = 'center'; // Pin button const pinButton = document.createElement('button'); pinButton.textContent = note.pinned ? '📌' : '📍'; pinButton.title = note.pinned ? 'Unpin' : 'Pin'; pinButton.style.cssText = ` background: transparent; border: none; color: #fff; cursor: pointer; padding: 0; font-size: 16px; opacity: ${note.pinned ? '1' : '0.5'}; `; pinButton.addEventListener('click', async (e) => { e.stopPropagation(); await this.togglePin(note); }); controls.appendChild(pinButton); // Collapse indicator const collapseIcon = document.createElement('span'); collapseIcon.textContent = '▼'; collapseIcon.style.transition = 'transform 0.2s'; controls.appendChild(collapseIcon); header.appendChild(title); header.appendChild(controls); // Content container const contentContainer = document.createElement('div'); contentContainer.style.cssText = ` background: #2a2a2a; border-top: 1px solid #555; `; // Mode toggle (for markdown notes) let isEditMode = false; let modeToggle = null; if (note.format_style === 'markdown') { const toggleBar = document.createElement('div'); toggleBar.style.cssText = ` padding: 5px 10px; background: #252525; border-bottom: 1px solid #555; display: flex; gap: 5px; `; const viewBtn = document.createElement('button'); viewBtn.textContent = 'View'; viewBtn.style.cssText = ` background: #007acc; color: white; border: none; padding: 3px 10px; cursor: pointer; border-radius: 3px; font-size: 11px; `; const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; editBtn.style.cssText = ` background: #555; color: white; border: none; padding: 3px 10px; cursor: pointer; border-radius: 3px; font-size: 11px; `; toggleBar.appendChild(viewBtn); toggleBar.appendChild(editBtn); contentContainer.appendChild(toggleBar); modeToggle = { viewBtn, editBtn }; } // View content (rendered markdown or plain text) const viewContent = document.createElement('div'); viewContent.style.cssText = ` padding: 10px; `; if (note.format_style === 'markdown') { viewContent.innerHTML = this.renderMarkdown(note.content); } else { viewContent.textContent = note.content; viewContent.style.whiteSpace = 'pre-wrap'; } // Edit content (textarea) const editContent = document.createElement('textarea'); editContent.value = note.content; editContent.style.cssText = ` width: 100%; min-height: 100px; padding: 10px; background: #1a1a1a; color: #fff; border: none; font-family: monospace; font-size: 12px; resize: vertical; box-sizing: border-box; display: none; `; // Save button const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style.cssText = ` margin: 5px 10px 10px 10px; background: #007acc; color: white; border: none; padding: 5px 15px; cursor: pointer; border-radius: 3px; display: none; `; saveBtn.addEventListener('click', async () => { await this.updateNoteContent(note.uuid, editContent.value); }); contentContainer.appendChild(viewContent); contentContainer.appendChild(editContent); contentContainer.appendChild(saveBtn); // Toggle between view and edit mode if (modeToggle) { const switchToView = () => { isEditMode = false; viewContent.style.display = 'block'; editContent.style.display = 'none'; saveBtn.style.display = 'none'; modeToggle.viewBtn.style.background = '#007acc'; modeToggle.editBtn.style.background = '#555'; }; const switchToEdit = () => { isEditMode = true; viewContent.style.display = 'none'; editContent.style.display = 'block'; saveBtn.style.display = 'block'; modeToggle.viewBtn.style.background = '#555'; modeToggle.editBtn.style.background = '#007acc'; }; modeToggle.viewBtn.addEventListener('click', (e) => { e.stopPropagation(); switchToView(); }); modeToggle.editBtn.addEventListener('click', (e) => { e.stopPropagation(); switchToEdit(); }); } else { // For plain text, click to edit directly viewContent.addEventListener('click', (e) => { e.stopPropagation(); viewContent.style.display = 'none'; editContent.style.display = 'block'; saveBtn.style.display = 'block'; editContent.focus(); }); } // Collapsible functionality let isCollapsed = false; header.addEventListener('click', () => { isCollapsed = !isCollapsed; contentContainer.style.display = isCollapsed ? 'none' : 'block'; collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; }); noteElement.appendChild(header); noteElement.appendChild(contentContainer); this.notesContainer.appendChild(noteElement); } renderMarkdown(text) { if (!text) return ''; // Use the bundled marked library if available if (this.marked) { try { return this.marked.parse(text, { breaks: true, gfm: true }); } catch (error) { console.error('Markdown rendering error:', error); } } // Fallback to plain text with line breaks return text.replace(/\n/g, '
'); } async addNote() { // Load folders first if not already loaded await this.loadFolders(); // Show custom dialog to get note name and folder const result = await this.showAddNoteDialog(); if (!result) return; try { const response = await api.fetchApi('/autonotes/notes', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: result.name, folder_uuid: result.folder_uuid }), }); const noteResult = await response.json(); if (noteResult.uuid) { await this.refreshNotes(); await this.openEditDialog(noteResult.uuid); } } catch (error) { console.error('Failed to create note:', error); alert('Failed to create note'); } } async showAddNoteDialog() { return new Promise((resolve) => { // Create overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 3000; display: flex; justify-content: center; align-items: center; `; // Create dialog const dialog = document.createElement('div'); dialog.style.cssText = ` background: #2a2a2a; border: 1px solid #555; border-radius: 8px; padding: 20px; width: 400px; color: #fff; font-family: Arial, sans-serif; `; // Title const title = document.createElement('h2'); title.textContent = 'Create New Note'; title.style.cssText = ` margin: 0 0 20px 0; font-size: 1.3em; `; // Name input const nameLabel = document.createElement('label'); nameLabel.textContent = 'Note Name:'; nameLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.placeholder = 'Enter note name...'; nameInput.style.cssText = ` width: 100%; padding: 8px; background: #1a1a1a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; margin-bottom: 20px; font-size: 14px; `; // Folder selection const folderLabel = document.createElement('label'); folderLabel.textContent = 'Folder:'; folderLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const folderTreeContainer = document.createElement('div'); folderTreeContainer.style.cssText = ` width: 100%; max-height: 200px; overflow-y: auto; background: #1a1a1a; border: 1px solid #555; border-radius: 4px; margin-bottom: 20px; padding: 8px; `; let selectedFolderUuid = null; // Root option const rootItem = document.createElement('div'); rootItem.style.cssText = ` padding: 5px 8px; cursor: pointer; border-radius: 3px; background: #007acc; margin-bottom: 5px; `; rootItem.textContent = '📁 [Root]'; rootItem.dataset.folderUuid = ''; rootItem.addEventListener('click', () => { // Remove selection from all items folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => { item.style.background = ''; }); rootItem.style.background = '#007acc'; selectedFolderUuid = null; }); folderTreeContainer.appendChild(rootItem); // Build folder tree recursively // level 0 = [Root] // level 1 = folders with parent_uuid = null (direct children of root) // level 2+ = nested folders const buildFolderTree = (parentUuid, level = 1) => { // Filter folders by parent - handle both null and undefined const childFolders = this.folders.filter(f => { const folderParent = f.parent_uuid || null; const searchParent = parentUuid || null; return folderParent === searchParent; }); for (const folder of childFolders) { const folderItem = document.createElement('div'); const indent = 8 + (level * 20); folderItem.style.cssText = ` padding: 5px 8px; padding-left: ${indent}px; cursor: pointer; border-radius: 3px; margin-bottom: 2px; `; folderItem.textContent = `${' '.repeat(level)}📁 ${folder.name}`; folderItem.dataset.folderUuid = folder.uuid; folderItem.addEventListener('click', () => { // Remove selection from all items folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => { item.style.background = ''; }); folderItem.style.background = '#007acc'; selectedFolderUuid = folder.uuid; }); folderTreeContainer.appendChild(folderItem); // Recursively build child folders buildFolderTree(folder.uuid, level + 1); } }; // Start building from root (null parent) at level 1 // Level 0 is [Root] itself, so children start at level 1 buildFolderTree(null, 1); // Buttons const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: flex-end; gap: 10px; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; cancelBtn.style.cssText = ` background: #666; color: white; border: none; padding: 8px 20px; cursor: pointer; border-radius: 4px; `; const createBtn = document.createElement('button'); createBtn.textContent = 'Create'; createBtn.style.cssText = ` background: #007acc; color: white; border: none; padding: 8px 20px; cursor: pointer; border-radius: 4px; `; // Event handlers const closeDialog = (result) => { overlay.remove(); resolve(result); }; cancelBtn.addEventListener('click', () => closeDialog(null)); createBtn.addEventListener('click', () => { const name = nameInput.value.trim(); if (!name) { alert('Please enter a note name'); return; } closeDialog({ name: name, folder_uuid: selectedFolderUuid }); }); // Allow Enter key to create nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { createBtn.click(); } }); // Focus name input setTimeout(() => nameInput.focus(), 0); // Assemble dialog buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(createBtn); dialog.appendChild(title); dialog.appendChild(nameLabel); dialog.appendChild(nameInput); dialog.appendChild(folderLabel); dialog.appendChild(folderTreeContainer); dialog.appendChild(buttonContainer); overlay.appendChild(dialog); document.body.appendChild(overlay); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeDialog(null); } }); }); } async openEditDialog(noteUuid = null) { if (this.editDialog) { this.editDialog.remove(); } await this.loadFolders(); // Load all notes for the edit dialog, regardless of display mode await this.loadAllNotes(); this.createEditDialog(noteUuid); } async loadAllNotes() { try { const response = await api.fetchApi('/autonotes/notes?mode=all'); this.notes = await response.json(); } catch (error) { console.error('Failed to load all notes:', error); } } async loadFolders() { try { const response = await api.fetchApi('/autonotes/folders'); this.folders = await response.json(); } catch (error) { console.error('Failed to load folders:', error); this.folders = []; } } createEditDialog(selectedNoteUuid = null) { // Create modal overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 2000; display: flex; justify-content: center; align-items: center; `; // Load saved dialog size const savedWidth = localStorage.getItem('autonotes_editDialogWidth') || '800px'; const savedHeight = localStorage.getItem('autonotes_editDialogHeight') || '600px'; // Create dialog this.editDialog = document.createElement('div'); this.editDialog.style.cssText = ` background: #2a2a2a; border: 1px solid #555; border-radius: 8px; width: ${savedWidth}; height: ${savedHeight}; display: flex; color: #fff; font-family: Arial, sans-serif; position: relative; `; // Track if we're currently resizing to prevent closing on overlay click this.isResizingDialog = false; // Create resize handles const createResizeHandle = (position, cursor) => { const handle = document.createElement('div'); handle.style.cssText = ` position: absolute; background: transparent; z-index: 10; `; if (position === 'right') { handle.style.cssText += ` right: 0; top: 0; width: 5px; height: 100%; cursor: ${cursor}; `; } else if (position === 'bottom') { handle.style.cssText += ` bottom: 0; left: 0; width: 100%; height: 5px; cursor: ${cursor}; `; } else if (position === 'corner') { handle.style.cssText += ` right: 0; bottom: 0; width: 15px; height: 15px; cursor: ${cursor}; `; } let isResizing = false; let startX, startY, startWidth, startHeight; handle.addEventListener('mousedown', (e) => { isResizing = true; this.isResizingDialog = true; startX = e.clientX; startY = e.clientY; startWidth = this.editDialog.offsetWidth; startHeight = this.editDialog.offsetHeight; document.body.style.userSelect = 'none'; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; if (position === 'right' || position === 'corner') { const newWidth = startWidth + (e.clientX - startX); this.editDialog.style.width = Math.max(400, Math.min(window.innerWidth - 40, newWidth)) + 'px'; } if (position === 'bottom' || position === 'corner') { const newHeight = startHeight + (e.clientY - startY); this.editDialog.style.height = Math.max(300, Math.min(window.innerHeight - 40, newHeight)) + 'px'; } }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; document.body.style.userSelect = ''; // Save size to localStorage localStorage.setItem('autonotes_editDialogWidth', this.editDialog.style.width); localStorage.setItem('autonotes_editDialogHeight', this.editDialog.style.height); // Delay clearing the flag to prevent overlay click from closing setTimeout(() => { this.isResizingDialog = false; }, 100); } }); return handle; }; this.editDialog.appendChild(createResizeHandle('right', 'ew-resize')); this.editDialog.appendChild(createResizeHandle('bottom', 'ns-resize')); this.editDialog.appendChild(createResizeHandle('corner', 'nwse-resize')); // Tree view (left side) const treeContainer = document.createElement('div'); treeContainer.style.cssText = ` width: 250px; border-right: 1px solid #555; padding: 10px; overflow-y: auto; `; const treeHeader = document.createElement('div'); treeHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; `; const treeTitle = document.createElement('h3'); treeTitle.textContent = 'Notes'; treeTitle.style.margin = '0'; const folderButtons = document.createElement('div'); folderButtons.style.display = 'flex'; folderButtons.style.gap = '5px'; const addFolderButton = document.createElement('button'); addFolderButton.textContent = '+'; addFolderButton.title = 'Add Folder'; addFolderButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 2px 8px; cursor: pointer; border-radius: 3px; font-size: 16px; `; addFolderButton.addEventListener('click', () => this.addFolder(treeContainer, selectedNoteUuid)); folderButtons.appendChild(addFolderButton); treeHeader.appendChild(treeTitle); treeHeader.appendChild(folderButtons); treeContainer.appendChild(treeHeader); this.renderNoteTree(treeContainer, selectedNoteUuid); // Make tree container a drop target for root treeContainer.addEventListener('dragover', (e) => { // Check if we're over empty space (not over a folder or note) const target = e.target; if (target === treeContainer || target === treeHeader || target === treeTitle) { e.preventDefault(); treeContainer.style.background = 'rgba(0, 122, 204, 0.1)'; } }); treeContainer.addEventListener('dragleave', (e) => { if (e.target === treeContainer) { treeContainer.style.background = ''; } }); treeContainer.addEventListener('drop', async (e) => { // Only handle if dropped on empty space const target = e.target; if (target === treeContainer || target === treeHeader || target === treeTitle) { e.preventDefault(); treeContainer.style.background = ''; const noteUuid = e.dataTransfer.getData('text/plain'); if (noteUuid) { await this.moveNoteToFolder(noteUuid, null, treeContainer, selectedNoteUuid); } } }); // Edit panel (right side) const editPanel = document.createElement('div'); editPanel.style.cssText = ` flex: 1; padding: 10px; display: flex; flex-direction: column; `; this.createEditPanel(editPanel, selectedNoteUuid); this.editDialog.appendChild(treeContainer); this.editDialog.appendChild(editPanel); overlay.appendChild(this.editDialog); document.body.appendChild(overlay); } renderNoteTree(container, selectedNoteUuid) { // Clear existing tree items (but not the header) const existingItems = container.querySelectorAll('.tree-item, .folder-item'); existingItems.forEach(item => item.remove()); // Render folders first for (const folder of this.folders) { const folderItem = this.createFolderItem(folder, container, selectedNoteUuid); container.appendChild(folderItem); } // Render notes (only top-level notes without folders for now) for (const note of this.notes) { if (!note.folder_uuid) { const noteItem = this.createNoteItem(note, selectedNoteUuid, container); container.appendChild(noteItem); } } } createFolderItem(folder, treeContainer, selectedNoteUuid) { const folderItem = document.createElement('div'); folderItem.className = 'folder-item'; folderItem.style.cssText = ` margin-bottom: 5px; `; const folderHeader = document.createElement('div'); folderHeader.style.cssText = ` display: flex; align-items: center; gap: 5px; padding: 5px; border-radius: 3px; background: #333; cursor: pointer; `; // Collapse/expand icon const expandIcon = document.createElement('span'); expandIcon.textContent = '▶'; expandIcon.style.cssText = ` transition: transform 0.2s; font-size: 10px; `; // Make folder a drop target folderHeader.addEventListener('dragover', (e) => { e.preventDefault(); folderHeader.style.background = '#007acc'; }); folderHeader.addEventListener('dragleave', (e) => { folderHeader.style.background = '#333'; }); folderHeader.addEventListener('drop', async (e) => { e.preventDefault(); folderHeader.style.background = '#333'; const noteUuid = e.dataTransfer.getData('text/plain'); if (noteUuid) { await this.moveNoteToFolder(noteUuid, folder.uuid, treeContainer, selectedNoteUuid); } }); const folderName = document.createElement('span'); folderName.textContent = `📁 ${folder.name}`; folderName.style.flex = '1'; const renameBtn = document.createElement('button'); renameBtn.textContent = '✏️'; renameBtn.title = 'Rename Folder'; renameBtn.style.cssText = ` background: transparent; border: none; color: #fff; cursor: pointer; padding: 2px; font-size: 12px; `; renameBtn.addEventListener('click', (e) => { e.stopPropagation(); this.renameFolder(folder, treeContainer, selectedNoteUuid); }); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '🗑️'; deleteBtn.title = 'Delete Folder'; deleteBtn.style.cssText = ` background: transparent; border: none; color: #fff; cursor: pointer; padding: 2px; font-size: 12px; `; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); this.deleteFolder(folder, treeContainer, selectedNoteUuid); }); folderHeader.appendChild(expandIcon); folderHeader.appendChild(folderName); folderHeader.appendChild(renameBtn); folderHeader.appendChild(deleteBtn); // Notes container (children) const notesContainer = document.createElement('div'); notesContainer.style.cssText = ` padding-left: 20px; display: none; `; // Find notes in this folder const folderNotes = this.notes.filter(note => note.folder_uuid === folder.uuid); for (const note of folderNotes) { const noteItem = this.createNoteItem(note, selectedNoteUuid, treeContainer); notesContainer.appendChild(noteItem); } // Toggle expand/collapse const isExpanded = this.expandedFolders.has(folder.uuid); notesContainer.style.display = isExpanded ? 'block' : 'none'; expandIcon.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'; folderHeader.addEventListener('click', (e) => { // Don't toggle if clicking buttons if (e.target === renameBtn || e.target === deleteBtn) { return; } if (this.expandedFolders.has(folder.uuid)) { this.expandedFolders.delete(folder.uuid); notesContainer.style.display = 'none'; expandIcon.style.transform = 'rotate(0deg)'; } else { this.expandedFolders.add(folder.uuid); notesContainer.style.display = 'block'; expandIcon.style.transform = 'rotate(90deg)'; } }); folderItem.appendChild(folderHeader); folderItem.appendChild(notesContainer); return folderItem; } createNoteItem(note, selectedNoteUuid, treeContainer) { const noteItem = document.createElement('div'); noteItem.className = 'tree-item'; noteItem.draggable = true; noteItem.style.cssText = ` padding: 5px; cursor: move; border-radius: 3px; margin-bottom: 2px; ${note.uuid === selectedNoteUuid ? 'background: #007acc;' : ''} `; noteItem.textContent = note.name || 'Untitled'; // Drag start noteItem.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', note.uuid); noteItem.style.opacity = '0.5'; }); // Drag end noteItem.addEventListener('dragend', (e) => { noteItem.style.opacity = '1'; }); // Left click to select noteItem.addEventListener('click', () => { // Remove previous selection treeContainer.querySelectorAll('.tree-item').forEach(item => { item.style.background = ''; }); noteItem.style.background = '#007acc'; this.loadNoteIntoEditor(note.uuid); }); // Right click for context menu noteItem.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showNoteContextMenu(e, note, treeContainer, selectedNoteUuid); }); return noteItem; } createEditPanel(panel, selectedNoteUuid) { // Content textarea const contentContainer = document.createElement('div'); contentContainer.style.cssText = ` flex: 1; display: flex; flex-direction: column; margin-bottom: 10px; `; const contentLabel = document.createElement('label'); contentLabel.textContent = 'Content: '; contentLabel.style.display = 'block'; contentLabel.style.marginBottom = '5px'; this.contentTextarea = document.createElement('textarea'); this.contentTextarea.style.cssText = ` flex: 1; padding: 10px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px; resize: none; font-family: monospace; `; contentContainer.appendChild(contentLabel); contentContainer.appendChild(this.contentTextarea); // Format style const formatContainer = document.createElement('div'); formatContainer.style.cssText = ` display: flex; gap: 20px; margin-bottom: 10px; align-items: center; `; const formatDiv = document.createElement('div'); const formatLabel = document.createElement('label'); formatLabel.textContent = 'Format: '; formatLabel.style.marginRight = '10px'; this.formatSelect = document.createElement('select'); this.formatSelect.style.cssText = ` background: #333; color: #fff; border: 1px solid #555; padding: 5px; `; this.formatSelect.innerHTML = ` `; formatDiv.appendChild(formatLabel); formatDiv.appendChild(this.formatSelect); // Pinned checkbox const pinnedDiv = document.createElement('div'); this.pinnedCheckbox = document.createElement('input'); this.pinnedCheckbox.type = 'checkbox'; this.pinnedCheckbox.id = 'note-pinned'; const pinnedLabel = document.createElement('label'); pinnedLabel.htmlFor = 'note-pinned'; pinnedLabel.textContent = 'Pinned'; pinnedLabel.style.marginLeft = '5px'; pinnedDiv.appendChild(this.pinnedCheckbox); pinnedDiv.appendChild(pinnedLabel); formatContainer.appendChild(formatDiv); formatContainer.appendChild(pinnedDiv); // Trigger conditions section const triggerContainer = document.createElement('div'); triggerContainer.style.cssText = ` margin-bottom: 10px; border: 1px solid #555; border-radius: 4px; padding: 10px; max-height: 200px; overflow-y: auto; `; const triggerTitle = document.createElement('h4'); triggerTitle.textContent = 'Trigger Conditions'; triggerTitle.style.margin = '0 0 10px 0'; const addTriggerButton = document.createElement('button'); addTriggerButton.textContent = 'Add Trigger'; addTriggerButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; margin-bottom: 10px; `; addTriggerButton.addEventListener('click', () => this.addTriggerCondition()); this.triggerList = document.createElement('div'); triggerContainer.appendChild(triggerTitle); triggerContainer.appendChild(addTriggerButton); triggerContainer.appendChild(this.triggerList); // Buttons const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: space-between; margin-top: 10px; `; const leftButtons = document.createElement('div'); leftButtons.style.display = 'flex'; leftButtons.style.gap = '10px'; const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; saveButton.addEventListener('click', () => this.saveCurrentNote()); const revertButton = document.createElement('button'); revertButton.textContent = 'Revert'; revertButton.style.cssText = ` background: #666; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; leftButtons.appendChild(saveButton); leftButtons.appendChild(revertButton); const rightButtons = document.createElement('div'); rightButtons.style.display = 'flex'; rightButtons.style.gap = '10px'; const okButton = document.createElement('button'); okButton.textContent = 'OK'; okButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; okButton.addEventListener('click', () => this.closeEditDialog(true)); const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` background: #666; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; cancelButton.addEventListener('click', () => this.closeEditDialog(false)); rightButtons.appendChild(okButton); rightButtons.appendChild(cancelButton); buttonContainer.appendChild(leftButtons); buttonContainer.appendChild(rightButtons); panel.appendChild(contentContainer); panel.appendChild(formatContainer); panel.appendChild(triggerContainer); panel.appendChild(buttonContainer); // Load initial note if specified if (selectedNoteUuid) { this.loadNoteIntoEditor(selectedNoteUuid); } } loadNoteIntoEditor(noteUuid) { const note = this.notes.find(n => n.uuid === noteUuid); if (!note) return; this.currentEditingNote = note; this.contentTextarea.value = note.content || ''; this.formatSelect.value = note.format_style || 'plaintext'; this.pinnedCheckbox.checked = note.pinned || false; // Load trigger conditions this.currentTriggerConditions = [...(note.trigger_conditions || [])]; this.renderTriggerConditions(); } addTriggerCondition() { const condition = { type: 'node_selected', node_types: [], node_type: null, attribute_name: null, attribute_values: [], workflow_names: [] }; this.currentTriggerConditions = this.currentTriggerConditions || []; this.currentTriggerConditions.push(condition); this.renderTriggerConditions(); } renderTriggerConditions() { this.triggerList.innerHTML = ''; if (!this.currentTriggerConditions) { this.currentTriggerConditions = []; } this.currentTriggerConditions.forEach((condition, index) => { const conditionElement = this.createTriggerConditionElement(condition, index); this.triggerList.appendChild(conditionElement); }); } createTriggerConditionElement(condition, index) { const container = document.createElement('div'); container.style.cssText = ` border: 1px solid #666; border-radius: 3px; margin-bottom: 10px; background: #333; `; // Header (collapsible) const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; padding: 10px; cursor: pointer; background: #3a3a3a; border-radius: 3px 3px 0 0; `; // Collapse icon const collapseIcon = document.createElement('span'); collapseIcon.textContent = '▼'; collapseIcon.style.cssText = ` transition: transform 0.2s; margin-right: 10px; font-size: 10px; `; // Title showing condition type const titleSpan = document.createElement('span'); titleSpan.style.flex = '1'; const getConditionTitle = () => { if (condition.type === 'node_selected') return 'Node Selected'; if (condition.type === 'node_attribute') return 'Node Attribute'; if (condition.type === 'node_in_workflow') return 'Node in Workflow'; if (condition.type === 'attribute_in_workflow') return 'Attribute in Workflow'; if (condition.type === 'workflow_name') return 'Workflow Name'; return 'Trigger Condition'; }; titleSpan.textContent = getConditionTitle(); header.appendChild(collapseIcon); header.appendChild(titleSpan); // Content container (collapsible) const content = document.createElement('div'); content.style.cssText = ` padding: 10px; display: block; `; // Type selector const typeContainer = document.createElement('div'); typeContainer.style.cssText = ` display: flex; align-items: center; margin-bottom: 10px; `; const typeLabel = document.createElement('label'); typeLabel.textContent = 'Type: '; typeLabel.style.marginRight = '10px'; const typeSelect = document.createElement('select'); typeSelect.style.cssText = ` background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 5px; margin-right: 10px; `; typeSelect.innerHTML = ` `; typeSelect.value = condition.type; const removeButton = document.createElement('button'); removeButton.textContent = 'Remove'; removeButton.style.cssText = ` background: #cc0000; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; margin-left: auto; `; removeButton.addEventListener('click', (e) => { e.stopPropagation(); this.currentTriggerConditions.splice(index, 1); this.renderTriggerConditions(); }); typeContainer.appendChild(typeLabel); typeContainer.appendChild(typeSelect); typeContainer.appendChild(removeButton); // Fields container for type-specific fields const fieldsContainer = document.createElement('div'); const updateContent = () => { condition.type = typeSelect.value; titleSpan.textContent = getConditionTitle(); fieldsContainer.innerHTML = ''; if (condition.type === 'node_selected') { this.createNodeSelectedFields(fieldsContainer, condition); } else if (condition.type === 'node_attribute') { this.createNodeAttributeFields(fieldsContainer, condition); } else if (condition.type === 'node_in_workflow') { this.createNodeInWorkflowFields(fieldsContainer, condition); } else if (condition.type === 'attribute_in_workflow') { this.createAttributeInWorkflowFields(fieldsContainer, condition); } else if (condition.type === 'workflow_name') { this.createWorkflowNameFields(fieldsContainer, condition); } }; typeSelect.addEventListener('change', updateContent); updateContent(); content.appendChild(typeContainer); content.appendChild(fieldsContainer); // Collapse/expand functionality let isCollapsed = false; header.addEventListener('click', (e) => { // Don't collapse if clicking on buttons/inputs if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') { return; } isCollapsed = !isCollapsed; content.style.display = isCollapsed ? 'none' : 'block'; collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; }); container.appendChild(header); container.appendChild(content); return container; } createNodeSelectedFields(container, condition) { const label = document.createElement('label'); label.textContent = 'Node Types: '; label.style.display = 'block'; label.style.marginBottom = '5px'; // Multi-select dropdown with searchable autocomplete const selectContainer = document.createElement('div'); selectContainer.style.cssText = ` position: relative; width: 100%; `; // Create a searchable input const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search node types...'; searchInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; margin-bottom: 5px; `; // Selected items display const selectedDisplay = document.createElement('div'); selectedDisplay.style.cssText = ` display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; min-height: 30px; padding: 5px; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; `; // Dropdown list const dropdownList = document.createElement('div'); dropdownList.style.cssText = ` position: absolute; width: 100%; max-height: 200px; overflow-y: auto; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; z-index: 1000; display: none; `; // Get node types from workflow const workflowNodeTypes = new Set(); if (app.graph && app.graph._nodes) { for (const node of app.graph._nodes) { workflowNodeTypes.add(node.type); } } // Populate dropdown with all node types const nodeTypesList = Object.keys(this.nodeTypes).sort(); const updateSelectedDisplay = () => { selectedDisplay.innerHTML = ''; (condition.node_types || []).forEach(nodeType => { const tag = document.createElement('span'); tag.style.cssText = ` background: #007acc; color: #fff; padding: 3px 8px; border-radius: 3px; display: inline-flex; align-items: center; gap: 5px; `; // Add icon if in workflow if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; tag.appendChild(icon); } const nameSpan = document.createElement('span'); nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; tag.appendChild(nameSpan); const removeBtn = document.createElement('span'); removeBtn.textContent = '×'; removeBtn.style.cursor = 'pointer'; removeBtn.style.fontWeight = 'bold'; removeBtn.addEventListener('click', () => { condition.node_types = condition.node_types.filter(t => t !== nodeType); updateSelectedDisplay(); }); tag.appendChild(removeBtn); selectedDisplay.appendChild(tag); }); }; const filterDropdown = (searchTerm) => { dropdownList.innerHTML = ''; const filtered = nodeTypesList.filter(nodeType => { const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; return displayName.toLowerCase().includes(searchTerm.toLowerCase()) || nodeType.toLowerCase().includes(searchTerm.toLowerCase()); }); filtered.forEach(nodeType => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px; cursor: pointer; display: flex; align-items: center; gap: 5px; `; // Add checkmark if in workflow if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; icon.style.fontWeight = 'bold'; option.appendChild(icon); } const nameSpan = document.createElement('span'); nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; option.appendChild(nameSpan); option.addEventListener('mouseenter', () => { option.style.background = '#007acc'; }); option.addEventListener('mouseleave', () => { option.style.background = ''; }); option.addEventListener('click', () => { if (!condition.node_types) condition.node_types = []; if (!condition.node_types.includes(nodeType)) { condition.node_types.push(nodeType); updateSelectedDisplay(); } searchInput.value = ''; dropdownList.style.display = 'none'; }); dropdownList.appendChild(option); }); dropdownList.style.display = filtered.length > 0 ? 'block' : 'none'; }; searchInput.addEventListener('focus', () => { filterDropdown(searchInput.value); }); searchInput.addEventListener('input', () => { filterDropdown(searchInput.value); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!selectContainer.contains(e.target)) { dropdownList.style.display = 'none'; } }); selectContainer.appendChild(searchInput); selectContainer.appendChild(dropdownList); container.appendChild(label); container.appendChild(selectedDisplay); container.appendChild(selectContainer); // Initialize display updateSelectedDisplay(); } createNodeAttributeFields(container, condition) { // Node type searchable dropdown const nodeTypeContainer = document.createElement('div'); nodeTypeContainer.style.marginBottom = '10px'; const nodeTypeLabel = document.createElement('label'); nodeTypeLabel.textContent = 'Node Type: '; nodeTypeLabel.style.display = 'block'; nodeTypeLabel.style.marginBottom = '5px'; // Get node types from workflow const workflowNodeTypes = new Set(); if (app.graph && app.graph._nodes) { for (const node of app.graph._nodes) { workflowNodeTypes.add(node.type); } } const selectContainer = document.createElement('div'); selectContainer.style.cssText = ` position: relative; width: 100%; `; // Display selected value const selectedDisplay = document.createElement('div'); selectedDisplay.style.cssText = ` width: 100%; padding: 8px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; cursor: pointer; display: flex; align-items: center; gap: 5px; `; const updateSelectedDisplay = () => { selectedDisplay.innerHTML = ''; if (condition.node_type) { if (workflowNodeTypes.has(condition.node_type)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; icon.style.fontWeight = 'bold'; selectedDisplay.appendChild(icon); } const text = document.createElement('span'); text.textContent = this.nodeTypes[condition.node_type]?.display_name || condition.node_type; selectedDisplay.appendChild(text); } else { const text = document.createElement('span'); text.textContent = '-- Select Node Type --'; text.style.color = '#888'; selectedDisplay.appendChild(text); } }; // Search input const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search node types...'; searchInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; display: none; `; // Dropdown list const dropdownList = document.createElement('div'); dropdownList.style.cssText = ` position: absolute; width: 100%; max-height: 200px; overflow-y: auto; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; z-index: 1000; display: none; margin-top: 2px; `; const nodeTypesList = Object.keys(this.nodeTypes).sort(); const filterDropdown = (searchTerm) => { dropdownList.innerHTML = ''; const filtered = nodeTypesList.filter(nodeType => { const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; return displayName.toLowerCase().includes(searchTerm.toLowerCase()) || nodeType.toLowerCase().includes(searchTerm.toLowerCase()); }); filtered.forEach(nodeType => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px; cursor: pointer; display: flex; align-items: center; gap: 5px; `; if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; icon.style.fontWeight = 'bold'; option.appendChild(icon); } const nameSpan = document.createElement('span'); nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; option.appendChild(nameSpan); option.addEventListener('mouseenter', () => { option.style.background = '#007acc'; }); option.addEventListener('mouseleave', () => { option.style.background = ''; }); option.addEventListener('click', () => { condition.node_type = nodeType; updateSelectedDisplay(); updateAttributeOptions(nodeType); dropdownList.style.display = 'none'; searchInput.style.display = 'none'; selectedDisplay.style.display = 'flex'; }); dropdownList.appendChild(option); }); dropdownList.style.display = filtered.length > 0 ? 'block' : 'none'; }; selectedDisplay.addEventListener('click', () => { selectedDisplay.style.display = 'none'; searchInput.style.display = 'block'; searchInput.focus(); filterDropdown(''); }); searchInput.addEventListener('input', () => { filterDropdown(searchInput.value); }); searchInput.addEventListener('blur', () => { setTimeout(() => { searchInput.style.display = 'none'; selectedDisplay.style.display = 'flex'; dropdownList.style.display = 'none'; }, 200); }); selectContainer.appendChild(selectedDisplay); selectContainer.appendChild(searchInput); selectContainer.appendChild(dropdownList); nodeTypeContainer.appendChild(nodeTypeLabel); nodeTypeContainer.appendChild(selectContainer); // Attribute name dropdown (populated based on selected node) const attrNameContainer = document.createElement('div'); attrNameContainer.style.marginBottom = '10px'; const attrNameLabel = document.createElement('label'); attrNameLabel.textContent = 'Attribute Name: '; attrNameLabel.style.display = 'block'; attrNameLabel.style.marginBottom = '5px'; const attrNameSelect = document.createElement('select'); attrNameSelect.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; `; // Function to update attribute dropdown based on selected node type const updateAttributeOptions = (nodeType) => { attrNameSelect.innerHTML = ''; // Add empty option const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = '-- Select Attribute --'; attrNameSelect.appendChild(emptyOption); if (nodeType && this.nodeTypes[nodeType]) { const nodeInfo = this.nodeTypes[nodeType]; const inputs = nodeInfo.input?.required || {}; const optionalInputs = nodeInfo.input?.optional || {}; // Add required inputs for (const [inputName, inputDef] of Object.entries(inputs)) { const option = document.createElement('option'); option.value = inputName; option.textContent = inputName; if (condition.attribute_name === inputName) { option.selected = true; } attrNameSelect.appendChild(option); } // Add optional inputs for (const [inputName, inputDef] of Object.entries(optionalInputs)) { const option = document.createElement('option'); option.value = inputName; option.textContent = `${inputName} (optional)`; if (condition.attribute_name === inputName) { option.selected = true; } attrNameSelect.appendChild(option); } } }; attrNameSelect.addEventListener('change', () => { condition.attribute_name = attrNameSelect.value; }); // Initialize displays updateSelectedDisplay(); updateAttributeOptions(condition.node_type); attrNameContainer.appendChild(attrNameLabel); attrNameContainer.appendChild(attrNameSelect); // Attribute values const attrValuesContainer = document.createElement('div'); attrValuesContainer.style.marginBottom = '10px'; const attrValuesLabel = document.createElement('label'); attrValuesLabel.textContent = 'Attribute Values (comma-separated): '; attrValuesLabel.style.display = 'block'; attrValuesLabel.style.marginBottom = '5px'; const attrValuesInput = document.createElement('input'); attrValuesInput.type = 'text'; attrValuesInput.value = (condition.attribute_values || []).join(', '); attrValuesInput.placeholder = 'e.g., model1.safetensors, model2.ckpt'; attrValuesInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; `; attrValuesInput.addEventListener('input', () => { condition.attribute_values = attrValuesInput.value.split(',').map(s => s.trim()).filter(s => s); }); attrValuesContainer.appendChild(attrValuesLabel); attrValuesContainer.appendChild(attrValuesInput); container.appendChild(nodeTypeContainer); container.appendChild(attrNameContainer); container.appendChild(attrValuesContainer); } createWorkflowNameFields(container, condition) { const label = document.createElement('label'); label.textContent = 'Workflow Names (comma-separated): '; label.style.display = 'block'; label.style.marginBottom = '5px'; const input = document.createElement('input'); input.type = 'text'; input.value = (condition.workflow_names || []).join(', '); input.placeholder = 'e.g., portrait, landscape, character'; input.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; `; input.addEventListener('input', () => { condition.workflow_names = input.value.split(',').map(s => s.trim()).filter(s => s); }); container.appendChild(label); container.appendChild(input); } createNodeInWorkflowFields(container, condition) { const label = document.createElement('label'); label.textContent = 'Node Types: '; label.style.display = 'block'; label.style.marginBottom = '5px'; // Multi-select dropdown with searchable autocomplete const selectContainer = document.createElement('div'); selectContainer.style.cssText = ` position: relative; width: 100%; `; // Create a searchable input const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search node types...'; searchInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; margin-bottom: 5px; `; // Selected items display const selectedDisplay = document.createElement('div'); selectedDisplay.style.cssText = ` display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; min-height: 30px; padding: 5px; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; `; // Dropdown list const dropdownList = document.createElement('div'); dropdownList.style.cssText = ` position: absolute; width: 100%; max-height: 200px; overflow-y: auto; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; z-index: 1000; display: none; `; // Get node types from workflow const workflowNodeTypes = new Set(); if (app.graph && app.graph._nodes) { for (const node of app.graph._nodes) { workflowNodeTypes.add(node.type); } } // Populate dropdown with all node types const nodeTypesList = Object.keys(this.nodeTypes).sort(); const updateSelectedDisplay = () => { selectedDisplay.innerHTML = ''; (condition.node_types || []).forEach(nodeType => { const tag = document.createElement('span'); tag.style.cssText = ` background: #007acc; color: #fff; padding: 3px 8px; border-radius: 3px; display: inline-flex; align-items: center; gap: 5px; `; // Add icon if in workflow if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; tag.appendChild(icon); } const nameSpan = document.createElement('span'); nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; tag.appendChild(nameSpan); const removeBtn = document.createElement('button'); removeBtn.textContent = '×'; removeBtn.style.cssText = ` background: transparent; border: none; color: #fff; cursor: pointer; padding: 0 3px; font-size: 16px; `; removeBtn.addEventListener('click', () => { condition.node_types = condition.node_types.filter(t => t !== nodeType); updateSelectedDisplay(); }); tag.appendChild(removeBtn); selectedDisplay.appendChild(tag); }); }; const filterDropdown = (searchTerm) => { dropdownList.innerHTML = ''; const lowerSearch = searchTerm.toLowerCase(); const filtered = nodeTypesList.filter(nodeType => { const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; return displayName.toLowerCase().includes(lowerSearch) && !(condition.node_types || []).includes(nodeType); }); if (filtered.length === 0) { dropdownList.style.display = 'none'; return; } filtered.forEach(nodeType => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px; cursor: pointer; border-bottom: 1px solid #555; display: flex; align-items: center; gap: 5px; `; // Add icon if in workflow if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; option.appendChild(icon); } const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; const textSpan = document.createElement('span'); textSpan.textContent = displayName; option.appendChild(textSpan); option.addEventListener('mouseover', () => { option.style.background = '#007acc'; }); option.addEventListener('mouseout', () => { option.style.background = ''; }); option.addEventListener('click', () => { condition.node_types = condition.node_types || []; condition.node_types.push(nodeType); searchInput.value = ''; updateSelectedDisplay(); filterDropdown(''); searchInput.focus(); }); dropdownList.appendChild(option); }); dropdownList.style.display = 'block'; }; searchInput.addEventListener('focus', () => { filterDropdown(searchInput.value); }); searchInput.addEventListener('input', () => { filterDropdown(searchInput.value); }); searchInput.addEventListener('blur', () => { setTimeout(() => { dropdownList.style.display = 'none'; }, 200); }); updateSelectedDisplay(); selectContainer.appendChild(searchInput); selectContainer.appendChild(dropdownList); container.appendChild(label); container.appendChild(selectedDisplay); container.appendChild(selectContainer); } createAttributeInWorkflowFields(container, condition) { const nodeTypeLabel = document.createElement('label'); nodeTypeLabel.textContent = 'Node Type: '; nodeTypeLabel.style.display = 'block'; nodeTypeLabel.style.marginBottom = '5px'; // Node Type dropdown with search const nodeTypeContainer = document.createElement('div'); nodeTypeContainer.style.cssText = ` position: relative; width: 100%; margin-bottom: 10px; `; const nodeTypeInput = document.createElement('input'); nodeTypeInput.type = 'text'; nodeTypeInput.placeholder = 'Search node type...'; nodeTypeInput.value = condition.node_type ? (this.nodeTypes[condition.node_type]?.display_name || condition.node_type) : ''; nodeTypeInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; `; const nodeTypeDropdown = document.createElement('div'); nodeTypeDropdown.style.cssText = ` position: absolute; width: 100%; max-height: 200px; overflow-y: auto; background: #2a2a2a; border: 1px solid #555; border-radius: 3px; z-index: 1000; display: none; `; // Get node types from workflow const workflowNodeTypes = new Set(); if (app.graph && app.graph._nodes) { for (const node of app.graph._nodes) { workflowNodeTypes.add(node.type); } } const nodeTypesList = Object.keys(this.nodeTypes).sort(); const filterNodeTypeDropdown = (searchTerm) => { nodeTypeDropdown.innerHTML = ''; const lowerSearch = searchTerm.toLowerCase(); const filtered = nodeTypesList.filter(nodeType => { const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; return displayName.toLowerCase().includes(lowerSearch); }); if (filtered.length === 0) { nodeTypeDropdown.style.display = 'none'; return; } filtered.forEach(nodeType => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px; cursor: pointer; border-bottom: 1px solid #555; display: flex; align-items: center; gap: 5px; `; // Add icon if in workflow if (workflowNodeTypes.has(nodeType)) { const icon = document.createElement('span'); icon.textContent = '✓'; icon.style.color = '#0f0'; option.appendChild(icon); } const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; const textSpan = document.createElement('span'); textSpan.textContent = displayName; option.appendChild(textSpan); option.addEventListener('mouseover', () => { option.style.background = '#007acc'; }); option.addEventListener('mouseout', () => { option.style.background = ''; }); option.addEventListener('click', () => { condition.node_type = nodeType; nodeTypeInput.value = displayName; nodeTypeDropdown.style.display = 'none'; // Refresh attribute dropdown updateAttributeDropdown(); }); nodeTypeDropdown.appendChild(option); }); nodeTypeDropdown.style.display = 'block'; }; nodeTypeInput.addEventListener('focus', () => { filterNodeTypeDropdown(nodeTypeInput.value); }); nodeTypeInput.addEventListener('input', () => { filterNodeTypeDropdown(nodeTypeInput.value); }); nodeTypeInput.addEventListener('blur', () => { setTimeout(() => { nodeTypeDropdown.style.display = 'none'; }, 200); }); nodeTypeContainer.appendChild(nodeTypeInput); nodeTypeContainer.appendChild(nodeTypeDropdown); // Attribute Name dropdown const attrLabel = document.createElement('label'); attrLabel.textContent = 'Attribute Name: '; attrLabel.style.display = 'block'; attrLabel.style.marginBottom = '5px'; attrLabel.style.marginTop = '10px'; const attrSelect = document.createElement('select'); attrSelect.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; margin-bottom: 10px; `; const updateAttributeDropdown = () => { attrSelect.innerHTML = ''; if (condition.node_type && this.nodeTypes[condition.node_type]) { const attributes = this.nodeTypes[condition.node_type].attributes || {}; Object.keys(attributes).forEach(attrName => { const option = document.createElement('option'); option.value = attrName; option.textContent = attrName; if (condition.attribute_name === attrName) { option.selected = true; } attrSelect.appendChild(option); }); } }; attrSelect.addEventListener('change', () => { condition.attribute_name = attrSelect.value || null; }); updateAttributeDropdown(); // Attribute Values const valuesLabel = document.createElement('label'); valuesLabel.textContent = 'Attribute Values (comma-separated, optional): '; valuesLabel.style.display = 'block'; valuesLabel.style.marginBottom = '5px'; const valuesInput = document.createElement('input'); valuesInput.type = 'text'; valuesInput.value = (condition.attribute_values || []).join(', '); valuesInput.placeholder = 'e.g., value1, value2'; valuesInput.style.cssText = ` width: 100%; padding: 5px; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 3px; `; valuesInput.addEventListener('input', () => { condition.attribute_values = valuesInput.value.split(',').map(s => s.trim()).filter(s => s); }); container.appendChild(nodeTypeLabel); container.appendChild(nodeTypeContainer); container.appendChild(attrLabel); container.appendChild(attrSelect); container.appendChild(valuesLabel); container.appendChild(valuesInput); } async saveCurrentNote() { if (!this.currentEditingNote) return false; // Clean up trigger conditions - remove null values const cleanedTriggerConditions = (this.currentTriggerConditions || []).map(tc => { const cleaned = { type: tc.type }; if (tc.node_types !== null && tc.node_types !== undefined) cleaned.node_types = tc.node_types; if (tc.node_type !== null && tc.node_type !== undefined) cleaned.node_type = tc.node_type; if (tc.attribute_name !== null && tc.attribute_name !== undefined) cleaned.attribute_name = tc.attribute_name; if (tc.attribute_values !== null && tc.attribute_values !== undefined) cleaned.attribute_values = tc.attribute_values; if (tc.workflow_names !== null && tc.workflow_names !== undefined) cleaned.workflow_names = tc.workflow_names; return cleaned; }); const data = { content: this.contentTextarea.value, format_style: this.formatSelect.value, pinned: this.pinnedCheckbox.checked, trigger_conditions: cleanedTriggerConditions }; try { const response = await api.fetchApi(`/autonotes/notes/${this.currentEditingNote.uuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); const result = await response.json(); if (result.success) { // Update local copy Object.assign(this.currentEditingNote, data); await this.refreshNotes(); return true; } else { alert('Failed to save note'); return false; } } catch (error) { console.error('Failed to save note:', error); alert('Failed to save note'); return false; } } async closeEditDialog(save = false) { if (save && this.currentEditingNote) { await this.saveCurrentNote(); } if (this.editDialog) { this.editDialog.parentElement.remove(); // Remove overlay this.editDialog = null; this.currentEditingNote = null; } } async togglePin(note) { try { const newPinnedState = !note.pinned; const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: note.content, format_style: note.format_style, pinned: newPinnedState, trigger_conditions: note.trigger_conditions || [] }), }); const result = await response.json(); if (result.success) { note.pinned = newPinnedState; await this.refreshNotes(); } else { alert('Failed to toggle pin'); } } catch (error) { console.error('Failed to toggle pin:', error); alert('Failed to toggle pin'); } } async addFolder(treeContainer, selectedNoteUuid) { const name = prompt('Enter folder name:'); if (!name) return; try { const response = await api.fetchApi('/autonotes/folders', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name }), }); const result = await response.json(); if (result.uuid) { await this.loadFolders(); this.renderNoteTree(treeContainer, selectedNoteUuid); } } catch (error) { console.error('Failed to create folder:', error); alert('Failed to create folder'); } } async renameFolder(folder, treeContainer, selectedNoteUuid) { const newName = prompt('Enter new folder name:', folder.name); if (!newName || newName === folder.name) return; try { const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newName }), }); const result = await response.json(); if (result.success) { await this.loadFolders(); this.renderNoteTree(treeContainer, selectedNoteUuid); } else { alert('Failed to rename folder'); } } catch (error) { console.error('Failed to rename folder:', error); alert('Failed to rename folder'); } } async deleteFolder(folder, treeContainer, selectedNoteUuid) { if (!confirm(`Delete folder "${folder.name}"? Notes inside will not be deleted.`)) { return; } try { const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, { method: 'DELETE', }); const result = await response.json(); if (result.success) { await this.loadFolders(); this.renderNoteTree(treeContainer, selectedNoteUuid); } else { alert('Failed to delete folder'); } } catch (error) { console.error('Failed to delete folder:', error); alert('Failed to delete folder'); } } async moveNoteToFolder(noteUuid, folderUuid, treeContainer, selectedNoteUuid) { try { const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ folder_uuid: folderUuid }), }); const result = await response.json(); if (result.success) { await this.refreshNotes(); this.renderNoteTree(treeContainer, selectedNoteUuid); } else { alert('Failed to move note'); } } catch (error) { console.error('Failed to move note:', error); alert('Failed to move note'); } } showNoteContextMenu(event, note, treeContainer, selectedNoteUuid) { // Remove any existing context menu const existingMenu = document.querySelector('.note-context-menu'); if (existingMenu) { existingMenu.remove(); } const menu = document.createElement('div'); menu.className = 'note-context-menu'; menu.style.cssText = ` position: fixed; left: ${event.clientX}px; top: ${event.clientY}px; background: #2a2a2a; border: 1px solid #555; border-radius: 4px; padding: 5px 0; z-index: 10000; min-width: 150px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); `; const renameOption = document.createElement('div'); renameOption.textContent = 'Rename'; renameOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; renameOption.addEventListener('mouseenter', () => { renameOption.style.background = '#007acc'; }); renameOption.addEventListener('mouseleave', () => { renameOption.style.background = ''; }); renameOption.addEventListener('click', async () => { menu.remove(); const newName = prompt('Enter new note name:', note.name); if (newName && newName !== note.name) { await this.renameNote(note.uuid, newName, treeContainer, selectedNoteUuid); } }); const deleteOption = document.createElement('div'); deleteOption.textContent = 'Delete'; deleteOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; deleteOption.addEventListener('mouseenter', () => { deleteOption.style.background = '#cc0000'; }); deleteOption.addEventListener('mouseleave', () => { deleteOption.style.background = ''; }); deleteOption.addEventListener('click', async () => { menu.remove(); if (confirm(`Delete note "${note.name}"?`)) { await this.deleteNote(note.uuid, treeContainer, selectedNoteUuid); } }); menu.appendChild(renameOption); menu.appendChild(deleteOption); document.body.appendChild(menu); // Close menu when clicking outside const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeMenu); } }; setTimeout(() => document.addEventListener('click', closeMenu), 0); } async renameNote(noteUuid, newName, treeContainer, selectedNoteUuid) { try { const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newName }), }); const result = await response.json(); if (result.success) { await this.refreshNotes(); this.renderNoteTree(treeContainer, selectedNoteUuid); } else { alert('Failed to rename note'); } } catch (error) { console.error('Failed to rename note:', error); alert('Failed to rename note'); } } async deleteNote(noteUuid, treeContainer, selectedNoteUuid) { try { const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { method: 'DELETE', }); const result = await response.json(); if (result.success) { await this.refreshNotes(); this.renderNoteTree(treeContainer, selectedNoteUuid); // Clear editor if deleted note was being edited if (this.currentEditingNote && this.currentEditingNote.uuid === noteUuid) { this.currentEditingNote = null; if (this.contentTextarea) { this.contentTextarea.value = ''; } } } else { alert('Failed to delete note'); } } catch (error) { console.error('Failed to delete note:', error); alert('Failed to delete note'); } } async updateNoteContent(noteUuid, content) { try { const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content }), }); const result = await response.json(); if (result.success) { // Refresh to show updated content await this.refreshNotes(); } else { alert('Failed to save note'); } } catch (error) { console.error('Failed to save note:', error); alert('Failed to save note'); } } } // Initialize the AutoNotes manager when the extension loads app.registerExtension({ name: "AutoNotes", async setup() { new AutoNotesManager(); }, });