diff --git a/CLAUDE.md b/CLAUDE.md index f9bf81c..30dc39b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,11 +35,17 @@ This is a ComfyUI extension that adds a server-side notes sidebar for managing m ## API Endpoints The extension exposes these REST endpoints: + +### Notes Management - `GET /notes_sidebar/list` - List all notes - `GET /notes_sidebar/note/{note_name}` - Get specific note content - `POST /notes_sidebar/note/{note_name}` - Save/create note - `DELETE /notes_sidebar/note/{note_name}` - Delete note +### LoRA Integration +- `GET /notes_sidebar/loras` - List all available LoRA files +- `GET /notes_sidebar/loras_with_metadata` - List LoRAs with associated metadata from `.txt` files + ## Development Notes ### Extension Structure @@ -56,10 +62,11 @@ The extension exposes these REST endpoints: - Active note tracking with tab highlighting - Content persistence across sidebar open/close cycles - Real-time tab rendering on note operations -- Button layout: Save/Rename on left, Refresh/Delete on right +- Button layout: Save/Rename/LoRA submenu on left, Refresh/Delete on right - **Unsaved Changes Tracking**: Visual indicators (*) for modified notes - **Content Preservation**: Tab switching preserves unsaved content - **Change Detection**: Automatic tracking of modifications vs saved state +- **LoRA Integration**: Submenu with three formatting options for appending LoRA lists ### Unsaved Changes Behavior - Notes with modifications show asterisk (*) in tab name @@ -74,6 +81,7 @@ The extension exposes these REST endpoints: - **API Design**: RESTful endpoints with JSON responses and consistent error handling - **UI Updates**: Event-driven architecture prevents manual DOM manipulation - **Extension Integration**: Follows ComfyUI's extension patterns (NODE_CLASS_MAPPINGS, registerExtension) +- **ComfyUI Integration**: Leverages `folder_paths` module for LoRA discovery and metadata loading ## Development Commands @@ -88,6 +96,8 @@ No automated tests defined. Manual testing workflow: 2. Verify sidebar tab appears with "Notes" label and book icon 3. Test CRUD operations: create, edit, save, rename, delete notes 4. Verify tab switching and content persistence +5. Test LoRA integration: verify submenu appears on click with three format options +6. Verify unsaved changes tracking with asterisk (*) indicators ### Development Workflow 1. Modify Python code in `__init__.py` for backend changes diff --git a/js/extension.js b/js/extension.js index c320103..b4d13d3 100644 --- a/js/extension.js +++ b/js/extension.js @@ -173,7 +173,7 @@ class NotesSidebar { } updateButtonState() { - if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendLorasBtn || !this.appendLorasTagsBtn || !this.appendLorasMetaBtn) return; + if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendLorasBtn) return; const hasActiveNote = !!this.activeNote; const hasUnsavedChanges = hasActiveNote && this.unsavedChanges.get(this.activeNote); @@ -185,8 +185,6 @@ class NotesSidebar { this.renameBtn.disabled = !hasActiveNote; this.deleteBtn.disabled = !hasActiveNote; this.appendLorasBtn.disabled = !hasActiveNote; - this.appendLorasTagsBtn.disabled = !hasActiveNote; - this.appendLorasMetaBtn.disabled = !hasActiveNote; } async init() { @@ -290,32 +288,48 @@ class NotesSidebar { this.deleteBtn.onclick = () => this.deleteActiveNote(); this.deleteBtn.disabled = true; + // LoRA submenu container + this.loraMenuContainer = document.createElement('div'); + this.loraMenuContainer.className = 'notes-submenu-container'; + this.appendLorasBtn = document.createElement('button'); this.appendLorasBtn.innerHTML = ''; this.appendLorasBtn.className = 'notes-btn append-loras'; - this.appendLorasBtn.title = 'Append LoRAs List'; - this.appendLorasBtn.onclick = () => this.appendLorasList(); + this.appendLorasBtn.title = 'Append'; + this.appendLorasBtn.onclick = () => this.toggleLoraMenu(); this.appendLorasBtn.disabled = true; - this.appendLorasTagsBtn = document.createElement('button'); - this.appendLorasTagsBtn.innerHTML = ''; - this.appendLorasTagsBtn.className = 'notes-btn append-loras-tags'; - this.appendLorasTagsBtn.title = 'Append LoRAs as Tags'; - this.appendLorasTagsBtn.onclick = () => this.appendLorasTags(); - this.appendLorasTagsBtn.disabled = true; + // Create submenu + this.loraSubmenu = document.createElement('div'); + this.loraSubmenu.className = 'notes-submenu'; + this.loraSubmenu.style.display = 'none'; - this.appendLorasMetaBtn = document.createElement('button'); - this.appendLorasMetaBtn.innerHTML = ''; - this.appendLorasMetaBtn.className = 'notes-btn append-loras-meta'; - this.appendLorasMetaBtn.title = 'Append LoRAs with Metadata'; - this.appendLorasMetaBtn.onclick = () => this.appendLorasWithMetadata(); - this.appendLorasMetaBtn.disabled = true; + // Create submenu items + const rawOption = document.createElement('div'); + rawOption.className = 'notes-submenu-item'; + rawOption.textContent = 'Append raw LoRA filenames list'; + rawOption.onclick = () => this.selectLoraOption('raw'); + + const tagOption = document.createElement('div'); + tagOption.className = 'notes-submenu-item'; + tagOption.textContent = "Append LoRAs in '' format"; + tagOption.onclick = () => this.selectLoraOption('tag'); + + const metaOption = document.createElement('div'); + metaOption.className = 'notes-submenu-item'; + metaOption.textContent = "Append LoRAs in ', metadata' format"; + metaOption.onclick = () => this.selectLoraOption('meta'); + + this.loraSubmenu.appendChild(rawOption); + this.loraSubmenu.appendChild(tagOption); + this.loraSubmenu.appendChild(metaOption); + + this.loraMenuContainer.appendChild(this.appendLorasBtn); + this.loraMenuContainer.appendChild(this.loraSubmenu); leftButtonContainer.appendChild(this.saveBtn); leftButtonContainer.appendChild(this.renameBtn); - leftButtonContainer.appendChild(this.appendLorasBtn); - leftButtonContainer.appendChild(this.appendLorasTagsBtn); - leftButtonContainer.appendChild(this.appendLorasMetaBtn); + leftButtonContainer.appendChild(this.loraMenuContainer); rightButtonContainer.appendChild(this.deleteBtn); buttonContainer.appendChild(leftButtonContainer); @@ -579,6 +593,41 @@ class NotesSidebar { this.store.setUnsavedChanges(this.activeNote, hasUnsavedChanges); } + toggleLoraMenu() { + if (this.loraSubmenu.style.display === 'none') { + this.loraSubmenu.style.display = 'block'; + // Add click listener to close menu when clicking outside + document.addEventListener('click', this.closeLoraMenuHandler); + } else { + this.loraSubmenu.style.display = 'none'; + document.removeEventListener('click', this.closeLoraMenuHandler); + } + } + + closeLoraMenuHandler = (event) => { + if (!this.loraMenuContainer.contains(event.target)) { + this.loraSubmenu.style.display = 'none'; + document.removeEventListener('click', this.closeLoraMenuHandler); + } + } + + selectLoraOption(type) { + this.loraSubmenu.style.display = 'none'; + document.removeEventListener('click', this.closeLoraMenuHandler); + + switch(type) { + case 'raw': + this.appendLorasList(); + break; + case 'tag': + this.appendLorasTags(); + break; + case 'meta': + this.appendLorasWithMetadata(); + break; + } + } + async appendLorasList() { try { const response = await fetch('/notes_sidebar/loras'); diff --git a/js/notes-sidebar.css b/js/notes-sidebar.css index a95be16..a0887d3 100644 --- a/js/notes-sidebar.css +++ b/js/notes-sidebar.css @@ -152,10 +152,38 @@ background: #9C27B0; } -.notes-btn.append-loras-tags { - background: #673AB7; +/* Submenu styles */ +.notes-submenu-container { + position: relative; + display: inline-block; } -.notes-btn.append-loras-meta { - background: #3F51B5; +.notes-submenu { + position: absolute; + top: 100%; + left: 0; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + z-index: 1000; + min-width: 200px; + margin-top: 2px; +} + +.notes-submenu-item { + padding: 8px 12px; + color: #fff; + cursor: pointer; + border-bottom: 1px solid #444; + font-size: 12px; + white-space: nowrap; +} + +.notes-submenu-item:last-child { + border-bottom: none; +} + +.notes-submenu-item:hover { + background: #444; } \ No newline at end of file