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.expandedNotes = new Set(); // Track which notes 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(); // Try to use ComfyUI's built-in sidebar tab API, fall back to legacy sidebar if (app.extensionManager?.registerSidebarTab) { this.registerAsSidebarTab(); } else { this.createLegacySidebar(); } 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 = {}; } } registerAsSidebarTab() { app.extensionManager.registerSidebarTab({ id: 'autonotes', icon: 'pi pi-book', title: 'AutoNotes', tooltip: 'AutoNotes - Workflow Documentation', type: 'custom', render: (container) => { this.buildSidebarContent(container); } }); } createLegacySidebar() { this.sidebar = document.createElement('div'); this.sidebar.id = 'autonotes-sidebar'; this.sidebar.style.cssText = ` position: fixed; right: 0; top: 0; width: 300px; height: 100vh; background: var(--comfy-menu-bg, #2a2a2a); border-left: 1px solid var(--border-color, #555); z-index: 1000; display: flex; flex-direction: column; box-sizing: border-box; font-family: Arial, sans-serif; color: var(--fg-color, #fff); `; this.buildSidebarContent(this.sidebar); document.body.appendChild(this.sidebar); } buildSidebarContent(container) { container.innerHTML = ''; // Main wrapper const wrapper = document.createElement('div'); wrapper.style.cssText = ` display: flex; flex-direction: column; height: 100%; padding: 10px; box-sizing: border-box; font-family: Arial, sans-serif; color: var(--fg-color, #fff); `; // Control panel const controlPanel = document.createElement('div'); controlPanel.style.cssText = ` margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color, #555); flex-shrink: 0; `; // 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: var(--comfy-input-bg, #333); color: var(--fg-color, #fff); border: 1px solid var(--border-color, #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); // Tag filter section const tagFilterContainer = document.createElement('div'); tagFilterContainer.style.marginBottom = '10px'; const tagFilterLabel = document.createElement('label'); tagFilterLabel.textContent = 'Filter by Tags: '; tagFilterLabel.style.cssText = ` display: block; margin-bottom: 5px; `; this.tagFilterSelect = document.createElement('div'); this.tagFilterSelect.style.cssText = ` background: var(--comfy-input-bg, #333); border: 1px solid var(--border-color, #555); padding: 5px; border-radius: 4px; min-height: 30px; cursor: pointer; position: relative; `; this.tagFilterSelect.textContent = 'Click to select tags...'; this.selectedTagFilters = new Set(); this.tagFilterSelect.addEventListener('click', (e) => { e.stopPropagation(); this.showTagFilterDropdown(); }); tagFilterContainer.appendChild(tagFilterLabel); tagFilterContainer.appendChild(this.tagFilterSelect); // Buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.flexWrap = 'wrap'; 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 addFromCurrentButton = document.createElement('button'); addFromCurrentButton.textContent = 'Add From Current Node'; addFromCurrentButton.style.cssText = ` background: #00a000; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; flex: 1; `; addFromCurrentButton.addEventListener('click', () => this.addNoteFromCurrentNode()); 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(addFromCurrentButton); buttonContainer.appendChild(editButton); controlPanel.appendChild(modeContainer); controlPanel.appendChild(tagFilterContainer); controlPanel.appendChild(buttonContainer); // Notes display area this.notesContainer = document.createElement('div'); this.notesContainer.style.cssText = ` flex: 1; overflow-y: auto; padding-right: 5px; `; wrapper.appendChild(controlPanel); wrapper.appendChild(this.notesContainer); container.appendChild(wrapper); // Render any already-loaded notes (tab may open after init fetch) if (this.notes.length > 0) { this.renderNotes(); } } 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() { if (!this.notesContainer) return; this.notesContainer.innerHTML = ''; // Apply tag filtering let filteredNotes = this.notes; if (this.selectedTagFilters.size > 0) { filteredNotes = this.notes.filter(note => { const noteTags = note.tags || []; // Note must have ALL selected tags (AND logic) return Array.from(this.selectedTagFilters).every(filterTag => noteTags.includes(filterTag) ); }); } if (filteredNotes.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.textContent = this.selectedTagFilters.size > 0 ? 'No notes match the selected tags' : '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 filteredNotes) { this.renderNote(note); } } showTagFilterDropdown() { // Collect all unique tags from all notes const allTags = new Set(); for (const note of this.notes) { if (note.tags) { note.tags.forEach(tag => allTags.add(tag)); } } if (allTags.size === 0) { alert('No tags found. Add tags to notes first.'); return; } // Remove existing dropdown const existingDropdown = document.querySelector('.tag-filter-dropdown'); if (existingDropdown) { existingDropdown.remove(); return; } // Create dropdown const dropdown = document.createElement('div'); dropdown.className = 'tag-filter-dropdown'; dropdown.style.cssText = ` position: absolute; top: 100%; left: 0; right: 0; background: #2a2a2a; border: 1px solid #555; border-radius: 4px; margin-top: 2px; max-height: 200px; overflow-y: auto; z-index: 1000; `; // Add "Clear All" option const clearOption = document.createElement('div'); clearOption.textContent = 'Clear All Filters'; clearOption.style.cssText = ` padding: 8px 10px; cursor: pointer; color: #f88; border-bottom: 1px solid #555; `; clearOption.addEventListener('mouseenter', () => { clearOption.style.background = '#333'; }); clearOption.addEventListener('mouseleave', () => { clearOption.style.background = ''; }); clearOption.addEventListener('click', () => { this.selectedTagFilters.clear(); this.updateTagFilterDisplay(); this.refreshNotes(); dropdown.remove(); }); dropdown.appendChild(clearOption); // Add tag options Array.from(allTags).sort().forEach(tag => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px 10px; cursor: pointer; display: flex; align-items: center; gap: 8px; `; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = this.selectedTagFilters.has(tag); const label = document.createElement('span'); label.textContent = tag; option.addEventListener('mouseenter', () => { option.style.background = '#333'; }); option.addEventListener('mouseleave', () => { option.style.background = ''; }); option.addEventListener('click', () => { if (this.selectedTagFilters.has(tag)) { this.selectedTagFilters.delete(tag); checkbox.checked = false; } else { this.selectedTagFilters.add(tag); checkbox.checked = true; } this.updateTagFilterDisplay(); this.refreshNotes(); }); option.appendChild(checkbox); option.appendChild(label); dropdown.appendChild(option); }); this.tagFilterSelect.style.position = 'relative'; this.tagFilterSelect.appendChild(dropdown); // Close dropdown when clicking outside const closeDropdown = (e) => { if (!this.tagFilterSelect.contains(e.target)) { dropdown.remove(); document.removeEventListener('click', closeDropdown); } }; setTimeout(() => { document.addEventListener('click', closeDropdown); }, 0); } updateTagFilterDisplay() { if (this.selectedTagFilters.size === 0) { this.tagFilterSelect.innerHTML = 'Click to select tags...'; this.tagFilterSelect.style.color = ''; } else { this.tagFilterSelect.innerHTML = ''; this.tagFilterSelect.style.color = '#fff'; this.tagFilterSelect.style.cssText += ` display: flex; flex-wrap: wrap; gap: 5px; `; Array.from(this.selectedTagFilters).forEach(tag => { const tagBadge = document.createElement('span'); tagBadge.style.cssText = ` background: #007acc; padding: 2px 6px; border-radius: 3px; font-size: 11px; display: inline-block; `; tagBadge.textContent = tag; this.tagFilterSelect.appendChild(tagBadge); }); // Re-add base styles this.tagFilterSelect.style.background = '#333'; this.tagFilterSelect.style.border = '1px solid #555'; this.tagFilterSelect.style.padding = '5px'; this.tagFilterSelect.style.borderRadius = '4px'; this.tagFilterSelect.style.minHeight = '30px'; this.tagFilterSelect.style.cursor = 'pointer'; this.tagFilterSelect.style.position = 'relative'; } } 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; `; // Tags section (at the top) const tagsSection = document.createElement('div'); tagsSection.style.cssText = ` padding: 10px; border-bottom: 1px solid #555; background: #252525; `; const tagsHeader = document.createElement('div'); tagsHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; `; const tagsLabel = document.createElement('span'); tagsLabel.textContent = 'Tags:'; tagsLabel.style.cssText = ` font-size: 11px; color: #aaa; `; const addTagBtn = document.createElement('button'); addTagBtn.textContent = '+ Tag'; addTagBtn.style.cssText = ` background: #007acc; color: white; border: none; padding: 2px 6px; cursor: pointer; border-radius: 3px; font-size: 10px; `; addTagBtn.addEventListener('click', async (e) => { e.stopPropagation(); const tag = prompt('Enter tag name:'); if (tag && tag.trim()) { const trimmedTag = tag.trim(); if (!note.tags) note.tags = []; if (!note.tags.includes(trimmedTag)) { note.tags.push(trimmedTag); await this.updateNoteTags(note); this.renderNotes(); } } }); tagsHeader.appendChild(tagsLabel); tagsHeader.appendChild(addTagBtn); const tagsContainer = document.createElement('div'); tagsContainer.style.cssText = ` display: flex; flex-wrap: wrap; gap: 5px; min-height: 20px; `; if (note.tags && note.tags.length > 0) { note.tags.forEach(tag => { const tagBadge = document.createElement('div'); tagBadge.style.cssText = ` background: #007acc; color: white; padding: 3px 6px; border-radius: 3px; font-size: 11px; display: flex; align-items: center; gap: 4px; `; const tagText = document.createElement('span'); tagText.textContent = tag; const removeBtn = document.createElement('button'); removeBtn.textContent = '×'; removeBtn.style.cssText = ` background: transparent; border: none; color: white; cursor: pointer; font-size: 14px; padding: 0; line-height: 1; `; removeBtn.addEventListener('click', async (e) => { e.stopPropagation(); note.tags = note.tags.filter(t => t !== tag); await this.updateNoteTags(note); this.renderNotes(); }); tagBadge.appendChild(tagText); tagBadge.appendChild(removeBtn); tagsContainer.appendChild(tagBadge); }); } else { const noTags = document.createElement('span'); noTags.textContent = 'No tags'; noTags.style.cssText = ` font-size: 11px; color: #666; font-style: italic; `; tagsContainer.appendChild(noTags); } tagsSection.appendChild(tagsHeader); tagsSection.appendChild(tagsContainer); contentContainer.appendChild(tagsSection); // 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) - resizable const viewContent = document.createElement('div'); viewContent.style.cssText = ` padding: 10px; min-height: 50px; max-height: none; overflow-y: auto; position: relative; resize: vertical; `; 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; `; // Auto-save function const autoSave = async () => { await this.updateNoteContent(note.uuid, editContent.value); note.content = editContent.value; // Update view content if (note.format_style === 'markdown') { viewContent.innerHTML = this.renderMarkdown(note.content); } else { viewContent.textContent = note.content; } }; saveBtn.addEventListener('click', async () => { await autoSave(); }); // Auto-save when clicking off the textarea editContent.addEventListener('blur', async () => { // Only auto-save if content has changed if (editContent.value !== note.content) { await autoSave(); } }); contentContainer.appendChild(viewContent); contentContainer.appendChild(editContent); contentContainer.appendChild(saveBtn); // Toggle between view and edit mode if (modeToggle) { const switchToView = async () => { // Auto-save before switching to view if (isEditMode && editContent.value !== note.content) { await autoSave(); } // Copy the current height from editContent to viewContent const currentHeight = editContent.offsetHeight; if (currentHeight > 0) { viewContent.style.height = currentHeight + 'px'; } 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; // Copy the current height from viewContent to editContent const currentHeight = viewContent.offsetHeight; if (currentHeight > 0) { editContent.style.height = currentHeight + 'px'; } 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', async (e) => { e.stopPropagation(); await switchToView(); }); modeToggle.editBtn.addEventListener('click', (e) => { e.stopPropagation(); switchToEdit(); }); } else { // For plain text, click to edit directly viewContent.addEventListener('click', (e) => { e.stopPropagation(); // Copy the current height from viewContent to editContent const currentHeight = viewContent.offsetHeight; if (currentHeight > 0) { editContent.style.height = currentHeight + 'px'; } viewContent.style.display = 'none'; editContent.style.display = 'block'; saveBtn.style.display = 'block'; editContent.focus(); }); } // Collapsible functionality - restore previous state const isExpanded = this.expandedNotes.has(note.uuid); let isCollapsed = !isExpanded; // Set initial state contentContainer.style.display = isCollapsed ? 'none' : 'block'; collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; header.addEventListener('click', () => { isCollapsed = !isCollapsed; contentContainer.style.display = isCollapsed ? 'none' : 'block'; collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; // Update tracking state if (isCollapsed) { this.expandedNotes.delete(note.uuid); } else { this.expandedNotes.add(note.uuid); } }); // Context menu for sidebar notes noteElement.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showSidebarNoteContextMenu(e, note); }); 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 addNoteFromCurrentNode() { // Check if a node is selected if (!this.selectedNodeType) { alert('Please select a node first'); return; } // Load folders first if not already loaded await this.loadFolders(); // Show dialog to configure trigger and note details const result = await this.showAddNoteFromCurrentNodeDialog(); if (!result) return; try { // Create the note 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) { // Update the note with the trigger condition await api.fetchApi(`/autonotes/notes/${noteResult.uuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ trigger_conditions: [result.triggerCondition] }), }); // Just refresh notes - don't open the edit dialog await this.refreshNotes(); } } 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 showAddNoteFromCurrentNodeDialog() { 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: 500px; max-height: 80vh; overflow-y: auto; color: #fff; font-family: Arial, sans-serif; `; // Title const title = document.createElement('h2'); title.textContent = 'Create Note from Current Node'; title.style.cssText = ` margin: 0 0 20px 0; font-size: 1.3em; `; // Current node info const nodeInfo = document.createElement('div'); nodeInfo.style.cssText = ` background: #1a1a1a; padding: 10px; border-radius: 4px; margin-bottom: 20px; border: 1px solid #555; `; const nodeInfoText = document.createElement('div'); nodeInfoText.textContent = `Selected Node: ${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}`; nodeInfoText.style.fontWeight = 'bold'; nodeInfo.appendChild(nodeInfoText); // 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.value = `Note for ${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}`; 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; `; // Trigger type selector const triggerTypeLabel = document.createElement('label'); triggerTypeLabel.textContent = 'Trigger Type:'; triggerTypeLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const triggerTypeSelect = document.createElement('select'); triggerTypeSelect.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; `; triggerTypeSelect.innerHTML = ` `; // Dynamic fields container const dynamicFieldsContainer = document.createElement('div'); dynamicFieldsContainer.style.marginBottom = '20px'; // Trigger condition object const triggerCondition = { type: 'node_selected', node_types: [this.selectedNodeType], node_type: this.selectedNodeType, attribute_name: null, attribute_values: [], workflow_names: [] }; // Function to update dynamic fields based on trigger type const updateDynamicFields = () => { dynamicFieldsContainer.innerHTML = ''; triggerCondition.type = triggerTypeSelect.value; if (triggerCondition.type === 'node_selected') { // Already set with current node type triggerCondition.node_types = [this.selectedNodeType]; const info = document.createElement('div'); info.style.cssText = ` background: #1a1a1a; padding: 10px; border-radius: 4px; border: 1px solid #555; color: #aaa; `; info.textContent = `This note will appear when "${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}" is selected.`; dynamicFieldsContainer.appendChild(info); } else if (triggerCondition.type === 'node_selected_attribute') { // Attribute selector const attrLabel = document.createElement('label'); attrLabel.textContent = 'Attribute:'; attrLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const attrSelect = document.createElement('select'); attrSelect.style.cssText = ` width: 100%; padding: 8px; background: #1a1a1a; color: #fff; border: 1px solid #555; border-radius: 4px; margin-bottom: 10px; font-size: 14px; `; // Populate with node attributes const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = '-- Select Attribute --'; attrSelect.appendChild(emptyOption); if (this.nodeTypes[this.selectedNodeType]) { const nodeInfo = this.nodeTypes[this.selectedNodeType]; const inputs = nodeInfo.input?.required || {}; const optionalInputs = nodeInfo.input?.optional || {}; const allInputs = { ...inputs, ...optionalInputs }; Object.keys(allInputs).forEach(attrName => { const option = document.createElement('option'); option.value = attrName; option.textContent = attrName; attrSelect.appendChild(option); }); } // Value input const valueLabel = document.createElement('label'); valueLabel.textContent = 'Attribute Value (to match):'; valueLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.placeholder = 'Enter value...'; valueInput.style.cssText = ` width: 100%; padding: 8px; background: #1a1a1a; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 14px; `; // Update value input when attribute changes attrSelect.addEventListener('change', () => { triggerCondition.attribute_name = attrSelect.value; // Pre-fill with current value if available if (this.selectedNodeAttributes[attrSelect.value] !== undefined) { valueInput.value = String(this.selectedNodeAttributes[attrSelect.value]); triggerCondition.attribute_values = [valueInput.value]; } else { valueInput.value = ''; triggerCondition.attribute_values = []; } }); valueInput.addEventListener('input', () => { triggerCondition.attribute_values = valueInput.value ? [valueInput.value] : []; }); dynamicFieldsContainer.appendChild(attrLabel); dynamicFieldsContainer.appendChild(attrSelect); dynamicFieldsContainer.appendChild(valueLabel); dynamicFieldsContainer.appendChild(valueInput); } else if (triggerCondition.type === 'node_in_workflow') { // Already set with current node type triggerCondition.node_types = [this.selectedNodeType]; const info = document.createElement('div'); info.style.cssText = ` background: #1a1a1a; padding: 10px; border-radius: 4px; border: 1px solid #555; color: #aaa; `; info.textContent = `This note will appear when "${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}" exists anywhere in the workflow.`; dynamicFieldsContainer.appendChild(info); } else if (triggerCondition.type === 'node_in_workflow_attribute') { // Attribute selector const attrLabel = document.createElement('label'); attrLabel.textContent = 'Attribute:'; attrLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const attrSelect = document.createElement('select'); attrSelect.style.cssText = ` width: 100%; padding: 8px; background: #1a1a1a; color: #fff; border: 1px solid #555; border-radius: 4px; margin-bottom: 10px; font-size: 14px; `; // Populate with node attributes const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = '-- Select Attribute --'; attrSelect.appendChild(emptyOption); if (this.nodeTypes[this.selectedNodeType]) { const nodeInfo = this.nodeTypes[this.selectedNodeType]; const inputs = nodeInfo.input?.required || {}; const optionalInputs = nodeInfo.input?.optional || {}; const allInputs = { ...inputs, ...optionalInputs }; Object.keys(allInputs).forEach(attrName => { const option = document.createElement('option'); option.value = attrName; option.textContent = attrName; attrSelect.appendChild(option); }); } // Value input (optional) const valueLabel = document.createElement('label'); valueLabel.textContent = 'Attribute Value (optional, leave blank for any):'; valueLabel.style.cssText = ` display: block; margin-bottom: 5px; `; const valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.placeholder = 'Enter value or leave blank...'; valueInput.style.cssText = ` width: 100%; padding: 8px; background: #1a1a1a; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 14px; `; // Update value input when attribute changes attrSelect.addEventListener('change', () => { triggerCondition.attribute_name = attrSelect.value; // Pre-fill with current value if available if (this.selectedNodeAttributes[attrSelect.value] !== undefined) { valueInput.value = String(this.selectedNodeAttributes[attrSelect.value]); triggerCondition.attribute_values = [valueInput.value]; } else { valueInput.value = ''; triggerCondition.attribute_values = []; } }); valueInput.addEventListener('input', () => { triggerCondition.attribute_values = valueInput.value ? [valueInput.value] : []; }); dynamicFieldsContainer.appendChild(attrLabel); dynamicFieldsContainer.appendChild(attrSelect); dynamicFieldsContainer.appendChild(valueLabel); dynamicFieldsContainer.appendChild(valueInput); } }; triggerTypeSelect.addEventListener('change', updateDynamicFields); updateDynamicFields(); // 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 = ` max-height: 150px; overflow-y: auto; background: #1a1a1a; border: 1px solid #555; border-radius: 4px; padding: 10px; margin-bottom: 20px; `; let selectedFolderUuid = null; // Build folder tree const buildFolderTree = (parentUuid, level) => { const children = this.folders.filter(f => f.parent_uuid === parentUuid); children.forEach(folder => { const folderOption = document.createElement('div'); folderOption.style.cssText = ` padding: 5px; padding-left: ${level * 15}px; cursor: pointer; border-radius: 3px; `; folderOption.textContent = `📁 ${folder.name}`; folderOption.addEventListener('mouseenter', () => { folderOption.style.background = '#007acc'; }); folderOption.addEventListener('mouseleave', () => { if (selectedFolderUuid !== folder.uuid) { folderOption.style.background = ''; } }); folderOption.addEventListener('click', () => { // Deselect previous folderTreeContainer.querySelectorAll('div').forEach(el => { el.style.background = ''; }); selectedFolderUuid = folder.uuid; folderOption.style.background = '#007acc'; }); folderTreeContainer.appendChild(folderOption); buildFolderTree(folder.uuid, level + 1); }); }; // Add root option const rootOption = document.createElement('div'); rootOption.style.cssText = ` padding: 5px; cursor: pointer; border-radius: 3px; background: #007acc; `; rootOption.textContent = '[Root]'; rootOption.addEventListener('mouseenter', () => { rootOption.style.background = '#007acc'; }); rootOption.addEventListener('mouseleave', () => { if (selectedFolderUuid !== null) { rootOption.style.background = ''; } }); rootOption.addEventListener('click', () => { folderTreeContainer.querySelectorAll('div').forEach(el => { el.style.background = ''; }); selectedFolderUuid = null; rootOption.style.background = '#007acc'; }); folderTreeContainer.appendChild(rootOption); 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 16px; cursor: pointer; border-radius: 4px; `; cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); resolve(null); }); const createBtn = document.createElement('button'); createBtn.textContent = 'Create'; createBtn.style.cssText = ` background: #007acc; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; `; createBtn.addEventListener('click', () => { const name = nameInput.value.trim(); if (!name) { alert('Please enter a note name'); return; } // Validate trigger condition if (triggerCondition.type === 'node_selected_attribute' || triggerCondition.type === 'node_in_workflow_attribute') { if (!triggerCondition.attribute_name) { alert('Please select an attribute'); return; } } document.body.removeChild(overlay); resolve({ name, folder_uuid: selectedFolderUuid, triggerCondition }); }); // Handle Enter key 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(nodeInfo); dialog.appendChild(nameLabel); dialog.appendChild(nameInput); dialog.appendChild(triggerTypeLabel); dialog.appendChild(triggerTypeSelect); dialog.appendChild(dynamicFieldsContainer); 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) { document.body.removeChild(overlay); resolve(null); } }); }); } 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('application/x-note-uuid'); const folderUuid = e.dataTransfer.getData('application/x-folder-uuid'); if (noteUuid) { await this.moveNoteToFolder(noteUuid, null, treeContainer, selectedNoteUuid); } else if (folderUuid) { await this.moveFolderToFolder(folderUuid, 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); // Auto-save when clicking anywhere in the dialog (except textarea) this.editDialog.addEventListener('click', async (e) => { if (e.target !== this.contentTextarea && this.currentEditingNote && this.textareaChanged) { await this.saveCurrentNote(); this.textareaChanged = false; } }); // Auto-save when clicking on the overlay (outside dialog) overlay.addEventListener('click', async (e) => { if (e.target === overlay && this.currentEditingNote && this.textareaChanged) { await this.saveCurrentNote(); this.textareaChanged = false; } }); // Global document-level click handler for auto-save (catches clicks outside dialog) this.globalAutoSaveHandler = async (e) => { // Only auto-save if dialog is open and there are changes if (this.editDialog && this.currentEditingNote && this.textareaChanged) { // Check if click is outside the dialog and not on the textarea if (!this.editDialog.contains(e.target) && e.target !== this.contentTextarea) { await this.saveCurrentNote(); this.textareaChanged = false; } } }; // Add the handler with a slight delay to avoid immediate triggering setTimeout(() => { document.addEventListener('click', this.globalAutoSaveHandler, true); }, 100); 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 only root-level folders (parent_uuid === null) const rootFolders = this.folders.filter(f => !f.parent_uuid); for (const folder of rootFolders) { const folderItem = this.createFolderItem(folder, container, selectedNoteUuid); container.appendChild(folderItem); } // Render notes (only top-level notes without folders) 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.draggable = true; 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 draggable folderHeader.addEventListener('dragstart', (e) => { e.stopPropagation(); e.dataTransfer.setData('application/x-folder-uuid', folder.uuid); e.dataTransfer.effectAllowed = 'move'; folderHeader.style.opacity = '0.5'; }); folderHeader.addEventListener('dragend', (e) => { folderHeader.style.opacity = '1'; }); // Make folder a drop target folderHeader.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); folderHeader.style.background = '#007acc'; }); folderHeader.addEventListener('dragleave', (e) => { e.stopPropagation(); folderHeader.style.background = '#333'; }); folderHeader.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); folderHeader.style.background = '#333'; // Check if it's a note or folder being dropped const noteUuid = e.dataTransfer.getData('application/x-note-uuid'); const folderUuid = e.dataTransfer.getData('application/x-folder-uuid'); if (noteUuid) { await this.moveNoteToFolder(noteUuid, folder.uuid, treeContainer, selectedNoteUuid); } else if (folderUuid && folderUuid !== folder.uuid) { // Prevent dropping a folder into itself await this.moveFolderToFolder(folderUuid, folder.uuid, treeContainer, selectedNoteUuid); } }); // Right-click context menu folderHeader.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); this.showFolderContextMenu(e, folder, 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); // Children container (child folders and notes) const childrenContainer = document.createElement('div'); childrenContainer.style.cssText = ` padding-left: 20px; display: none; `; // Find child folders const childFolders = this.folders.filter(f => f.parent_uuid === folder.uuid); for (const childFolder of childFolders) { const childFolderItem = this.createFolderItem(childFolder, treeContainer, selectedNoteUuid); childrenContainer.appendChild(childFolderItem); } // 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); childrenContainer.appendChild(noteItem); } // Toggle expand/collapse const isExpanded = this.expandedFolders.has(folder.uuid); childrenContainer.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); childrenContainer.style.display = 'none'; expandIcon.style.transform = 'rotate(0deg)'; } else { this.expandedFolders.add(folder.uuid); childrenContainer.style.display = 'block'; expandIcon.style.transform = 'rotate(90deg)'; } }); folderItem.appendChild(folderHeader); folderItem.appendChild(childrenContainer); 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('application/x-note-uuid', note.uuid); e.dataTransfer.effectAllowed = 'move'; noteItem.style.opacity = '0.5'; }); // Drag end noteItem.addEventListener('dragend', (e) => { noteItem.style.opacity = '1'; }); // Left click to select noteItem.addEventListener('click', async () => { // Auto-save current note before switching if (this.currentEditingNote && this.textareaChanged) { await this.saveCurrentNote(); this.textareaChanged = false; } // 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; `; // Track if content has changed this.textareaChanged = false; // Auto-save on input (debounced) let saveTimeout; this.contentTextarea.addEventListener('input', () => { this.textareaChanged = true; clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { if (this.currentEditingNote && this.textareaChanged) { await this.saveCurrentNote(); this.textareaChanged = false; } }, 1000); // Auto-save after 1 second of inactivity }); // Auto-save on blur (when clicking outside the textarea) this.contentTextarea.addEventListener('blur', async () => { if (this.currentEditingNote && this.textareaChanged) { clearTimeout(saveTimeout); await this.saveCurrentNote(); this.textareaChanged = false; } }); 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); // Tags section const tagsContainer = document.createElement('div'); tagsContainer.style.cssText = ` margin-bottom: 10px; border: 1px solid #555; border-radius: 4px; padding: 10px; `; const tagsHeader = document.createElement('div'); tagsHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; `; const tagsTitle = document.createElement('h4'); tagsTitle.textContent = 'Tags'; tagsTitle.style.margin = '0'; const addTagButton = document.createElement('button'); addTagButton.textContent = '+ Add Tag'; addTagButton.style.cssText = ` background: #007acc; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; font-size: 11px; `; tagsHeader.appendChild(tagsTitle); tagsHeader.appendChild(addTagButton); this.tagsListContainer = document.createElement('div'); this.tagsListContainer.style.cssText = ` display: flex; flex-wrap: wrap; gap: 5px; `; this.currentTags = []; addTagButton.addEventListener('click', () => { const tag = prompt('Enter tag name:'); if (tag && tag.trim()) { const trimmedTag = tag.trim(); if (!this.currentTags.includes(trimmedTag)) { this.currentTags.push(trimmedTag); this.renderTags(); this.textareaChanged = true; // Mark as changed for auto-save } } }); tagsContainer.appendChild(tagsHeader); tagsContainer.appendChild(this.tagsListContainer); // 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(tagsContainer); 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 tags this.currentTags = [...(note.tags || [])]; this.renderTags(); // Load trigger conditions this.currentTriggerConditions = [...(note.trigger_conditions || [])]; this.renderTriggerConditions(); } renderTags() { this.tagsListContainer.innerHTML = ''; for (const tag of this.currentTags) { const tagElement = document.createElement('div'); tagElement.style.cssText = ` background: #007acc; color: white; padding: 4px 8px; border-radius: 3px; font-size: 12px; display: flex; align-items: center; gap: 5px; `; const tagText = document.createElement('span'); tagText.textContent = tag; const removeBtn = document.createElement('button'); removeBtn.textContent = '×'; removeBtn.style.cssText = ` background: transparent; border: none; color: white; cursor: pointer; font-size: 16px; padding: 0; line-height: 1; `; removeBtn.addEventListener('click', () => { this.currentTags = this.currentTags.filter(t => t !== tag); this.renderTags(); this.textareaChanged = true; // Mark as changed for auto-save }); tagElement.appendChild(tagText); tagElement.appendChild(removeBtn); this.tagsListContainer.appendChild(tagElement); } } 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_selected_attribute') return 'Node Selected + Attribute Value'; if (condition.type === 'node_in_workflow') return 'Node In Workflow'; if (condition.type === 'node_in_workflow_attribute') return 'Node In Workflow + Attribute Value'; 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_selected_attribute') { this.createNodeAttributeFields(fieldsContainer, condition); } else if (condition.type === 'node_in_workflow') { this.createNodeInWorkflowFields(fieldsContainer, condition); } else if (condition.type === 'node_in_workflow_attribute') { 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, tags: this.currentTags || [], 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) { // Always auto-save if there are unsaved changes if (this.currentEditingNote && this.textareaChanged) { await this.saveCurrentNote(); this.textareaChanged = false; } else if (save && this.currentEditingNote) { await this.saveCurrentNote(); } // Remove global auto-save handler if (this.globalAutoSaveHandler) { document.removeEventListener('click', this.globalAutoSaveHandler, true); this.globalAutoSaveHandler = null; } 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'); } } async moveFolderToFolder(folderUuid, parentFolderUuid, treeContainer, selectedNoteUuid) { try { // Prevent circular references if (folderUuid === parentFolderUuid) { alert('Cannot move a folder into itself'); return; } // Check if parent is a descendant of the folder being moved const isDescendant = (checkFolderId, ancestorId) => { if (!checkFolderId) return false; if (checkFolderId === ancestorId) return true; const folder = this.folders.find(f => f.uuid === checkFolderId); if (!folder) return false; return isDescendant(folder.parent_uuid, ancestorId); }; if (isDescendant(parentFolderUuid, folderUuid)) { alert('Cannot move a folder into its own descendant'); return; } const response = await api.fetchApi(`/autonotes/folders/${folderUuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ parent_uuid: parentFolderUuid }), }); const result = await response.json(); if (result.success) { await this.loadFolders(); this.renderNoteTree(treeContainer, selectedNoteUuid); } else { alert('Failed to move folder'); } } catch (error) { console.error('Failed to move folder:', error); alert('Failed to move folder'); } } showFolderContextMenu(event, folder, treeContainer, selectedNoteUuid) { // Remove any existing context menu const existingMenu = document.querySelector('.folder-context-menu'); if (existingMenu) { existingMenu.remove(); } const menu = document.createElement('div'); menu.className = 'folder-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 createChildOption = document.createElement('div'); createChildOption.textContent = 'New Child Folder'; createChildOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; createChildOption.addEventListener('mouseenter', () => { createChildOption.style.background = '#007acc'; }); createChildOption.addEventListener('mouseleave', () => { createChildOption.style.background = ''; }); createChildOption.addEventListener('click', async () => { menu.remove(); const folderName = prompt('Enter folder name:'); if (folderName) { await this.createFolderWithParent(folderName, folder.uuid, treeContainer, selectedNoteUuid); } }); 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(); this.renameFolder(folder, treeContainer, selectedNoteUuid); }); const deleteOption = document.createElement('div'); deleteOption.textContent = 'Delete'; deleteOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #f88; `; deleteOption.addEventListener('mouseenter', () => { deleteOption.style.background = '#007acc'; }); deleteOption.addEventListener('mouseleave', () => { deleteOption.style.background = ''; }); deleteOption.addEventListener('click', async () => { menu.remove(); this.deleteFolder(folder, treeContainer, selectedNoteUuid); }); menu.appendChild(createChildOption); 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 createFolderWithParent(name, parentUuid, treeContainer, selectedNoteUuid) { try { const response = await api.fetchApi('/autonotes/folders', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, parent_uuid: parentUuid }), }); const result = await response.json(); if (result.uuid) { await this.loadFolders(); // Expand the parent folder this.expandedFolders.add(parentUuid); this.renderNoteTree(treeContainer, selectedNoteUuid); } } catch (error) { console.error('Failed to create folder:', error); alert('Failed to create folder'); } } 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 duplicateOption = document.createElement('div'); duplicateOption.textContent = 'Duplicate'; duplicateOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; duplicateOption.addEventListener('mouseenter', () => { duplicateOption.style.background = '#007acc'; }); duplicateOption.addEventListener('mouseleave', () => { duplicateOption.style.background = ''; }); duplicateOption.addEventListener('click', async () => { menu.remove(); const newName = prompt('Enter name for duplicated note:', note.name); if (newName) { await this.duplicateNote(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(duplicateOption); 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 duplicateNote(noteUuid, newName, treeContainer, selectedNoteUuid) { try { // Get the original note const originalNote = this.notes.find(n => n.uuid === noteUuid); if (!originalNote) { alert('Note not found'); return; } // Create a new note with the same folder const response = await api.fetchApi('/autonotes/notes', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newName, folder_uuid: originalNote.folder_uuid }), }); const noteResult = await response.json(); if (noteResult.uuid) { // Update the new note with all properties from the original await api.fetchApi(`/autonotes/notes/${noteResult.uuid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: originalNote.content, format_style: originalNote.format_style, pinned: false, // Don't duplicate the pinned state tags: [...(originalNote.tags || [])], trigger_conditions: [...(originalNote.trigger_conditions || [])] }), }); await this.refreshNotes(); if (treeContainer) { this.renderNoteTree(treeContainer, selectedNoteUuid); } } } catch (error) { console.error('Failed to duplicate note:', error); alert('Failed to duplicate note'); } } showSidebarNoteContextMenu(event, note) { // Remove any existing context menu const existingMenu = document.querySelector('.sidebar-note-context-menu'); if (existingMenu) { existingMenu.remove(); } const menu = document.createElement('div'); menu.className = 'sidebar-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 editOption = document.createElement('div'); editOption.textContent = 'Edit'; editOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; editOption.addEventListener('mouseenter', () => { editOption.style.background = '#007acc'; }); editOption.addEventListener('mouseleave', () => { editOption.style.background = ''; }); editOption.addEventListener('click', async () => { menu.remove(); await this.openEditDialog(note.uuid); }); const duplicateOption = document.createElement('div'); duplicateOption.textContent = 'Duplicate'; duplicateOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #fff; `; duplicateOption.addEventListener('mouseenter', () => { duplicateOption.style.background = '#007acc'; }); duplicateOption.addEventListener('mouseleave', () => { duplicateOption.style.background = ''; }); duplicateOption.addEventListener('click', async () => { menu.remove(); const newName = prompt('Enter name for duplicated note:', note.name); if (newName) { await this.duplicateNote(note.uuid, newName, null, null); } }); const deleteOption = document.createElement('div'); deleteOption.textContent = 'Delete'; deleteOption.style.cssText = ` padding: 8px 15px; cursor: pointer; color: #f88; `; 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}"?`)) { try { const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, { method: 'DELETE', }); const result = await response.json(); if (result.success) { await this.refreshNotes(); } else { alert('Failed to delete note'); } } catch (error) { console.error('Failed to delete note:', error); alert('Failed to delete note'); } } }); menu.appendChild(editOption); menu.appendChild(duplicateOption); 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 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'); } } async updateNoteTags(note) { try { 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: note.pinned, tags: note.tags || [], trigger_conditions: note.trigger_conditions || [] }), }); const result = await response.json(); if (result.success) { await this.refreshNotes(); } else { alert('Failed to update tags'); } } catch (error) { console.error('Failed to update tags:', error); alert('Failed to update tags'); } } } // Initialize the AutoNotes manager when the extension loads let autoNotesManagerInstance = null; app.registerExtension({ name: "AutoNotes", async setup() { autoNotesManagerInstance = new AutoNotesManager(); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { const menuOptions = getExtraMenuOptions?.apply(this, arguments); options.unshift({ content: "Create AutoNote for this node", callback: async () => { if (autoNotesManagerInstance) { // Set the selected node type to this node's type autoNotesManagerInstance.selectedNodeType = this.type; // Collect node attributes const nodeAttributes = {}; if (this.widgets) { for (const widget of this.widgets) { nodeAttributes[widget.name] = widget.value; } } autoNotesManagerInstance.selectedNodeAttributes = nodeAttributes; // Call the add note from current node function await autoNotesManagerInstance.addNoteFromCurrentNode(); } } }); return menuOptions; }; } });