From b72bc5ca1802bff6f1b12d00a6e8fdb5e1fe9918 Mon Sep 17 00:00:00 2001 From: Alexopus Date: Sun, 20 Jul 2025 01:08:13 +0200 Subject: [PATCH] Refactor for sanity --- CLAUDE.md | 37 +++- js/api/notesApi.js | 103 +++++++++ js/components/AppendMenu.js | 178 ++++++++++++++++ js/extension.js | 414 +++++------------------------------- js/stores/NotesStore.js | 92 ++++++++ js/utils/validation.js | 10 + 6 files changed, 467 insertions(+), 367 deletions(-) create mode 100644 js/api/notesApi.js create mode 100644 js/components/AppendMenu.js create mode 100644 js/stores/NotesStore.js create mode 100644 js/utils/validation.js diff --git a/CLAUDE.md b/CLAUDE.md index 30dc39b..b3b11fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,9 +15,12 @@ This is a ComfyUI extension that adds a server-side notes sidebar for managing m - **File Storage**: Notes stored as `.md` files in the `notes/` directory ### Frontend (JavaScript) -- **Extension Module**: `js/extension.js` - ComfyUI extension integration -- **NotesStore Class**: Event-driven state management using EventTarget pattern -- **NotesSidebar Class**: Main UI component managing tabs, content area, and user interactions +- **Extension Module**: `js/extension.js` - Main ComfyUI extension integration and UI orchestration +- **State Management**: `js/stores/NotesStore.js` - Event-driven state management using EventTarget pattern +- **Components**: + - `js/components/AppendMenu.js` - Append functionality (currently LoRA integration) with submenu UI +- **API Layer**: `js/api/notesApi.js` - Centralized API calls with consistent error handling +- **Utilities**: `js/utils/validation.js` - Filename validation helpers - **Styling**: `js/notes-sidebar.css` - Complete UI styling for dark theme integration ### Key Components @@ -66,7 +69,7 @@ The extension exposes these REST endpoints: - **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 +- **Append Integration**: Submenu with three formatting options for appending LoRA lists (extensible for other content types) ### Unsaved Changes Behavior - Notes with modifications show asterisk (*) in tab name @@ -106,8 +109,32 @@ No automated tests defined. Manual testing workflow: 4. **Python changes**: Restart ComfyUI completely to reload the extension 5. **JavaScript/CSS changes**: Browser refresh is sufficient +### Common Development Tasks +- **Adding new API endpoints**: Add route handlers in `__init__.py` and corresponding methods in `js/api/notesApi.js` +- **Modifying note storage**: Update `NotesManager` class methods in `__init__.py` +- **UI state changes**: Modify `NotesStore` class in `js/stores/NotesStore.js` +- **Append functionality changes**: Update `js/components/AppendMenu.js` and related backend endpoints +- **Adding new components**: Create new files in `js/components/` and import them in `extension.js` +- **API changes**: Update `js/api/notesApi.js` static methods and error handling + ### File Structure - `notes/` - Storage directory for markdown notes (auto-created) - `js/` - Frontend assets loaded by ComfyUI + - `extension.js` - Main extension entry point (~450 lines) + - `stores/NotesStore.js` - State management (~80 lines) + - `components/AppendMenu.js` - Append functionality (~170 lines) + - `api/notesApi.js` - API service layer (~90 lines) + - `utils/validation.js` - Validation utilities (~10 lines) + - `notes-sidebar.css` - UI styling - `__init__.py` - Main extension code -- `pyproject.toml` - Project metadata \ No newline at end of file +- `pyproject.toml` - Project metadata + +### Important Development Considerations +- **Filename validation**: Use `isValidFilename()` from `js/utils/validation.js` to validate user input +- **URL encoding**: Note names are URL-encoded in API calls for safe handling +- **Event-driven architecture**: UI updates are triggered by state changes through EventTarget pattern in `NotesStore` +- **Modular architecture**: Components are separated by concern (state, UI, API, utilities) +- **API centralization**: All fetch calls are centralized in `js/api/notesApi.js` with consistent error handling +- **ComfyUI integration**: Uses `folder_paths.get_filename_list("loras")` for LoRA discovery +- **Error handling**: All API endpoints include try/catch with JSON error responses +- **Import structure**: ES6 modules with clear dependency relationships \ No newline at end of file diff --git a/js/api/notesApi.js b/js/api/notesApi.js new file mode 100644 index 0000000..392fefc --- /dev/null +++ b/js/api/notesApi.js @@ -0,0 +1,103 @@ +export class NotesApi { + static async getNotesList() { + try { + const response = await fetch('/notes_sidebar/list'); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to fetch notes list'); + } + + return data.notes || []; + } catch (error) { + console.error('Error loading notes:', error); + throw error; + } + } + + static async getNoteContent(noteName) { + try { + const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to fetch note content'); + } + + return data.content || ''; + } catch (error) { + console.error('Error loading note content:', error); + throw error; + } + } + + static async saveNote(noteName, content) { + try { + const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: content }) + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to save note'); + } + + return true; + } catch (error) { + console.error('Error saving note:', error); + throw error; + } + } + + static async deleteNote(noteName) { + try { + const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`, { + method: 'DELETE' + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete note'); + } + + return true; + } catch (error) { + console.error('Error deleting note:', error); + throw error; + } + } + + static async getLorasList() { + try { + const response = await fetch('/notes_sidebar/loras'); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to fetch LoRAs list'); + } + + return data.loras || []; + } catch (error) { + console.error('Error fetching LoRAs:', error); + throw error; + } + } + + static async getLorasWithMetadata() { + try { + const response = await fetch('/notes_sidebar/loras_with_metadata'); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to fetch LoRAs with metadata'); + } + + return data.loras || []; + } catch (error) { + console.error('Error fetching LoRAs with metadata:', error); + throw error; + } + } +} \ No newline at end of file diff --git a/js/components/AppendMenu.js b/js/components/AppendMenu.js new file mode 100644 index 0000000..16aa586 --- /dev/null +++ b/js/components/AppendMenu.js @@ -0,0 +1,178 @@ +import { NotesApi } from "../api/notesApi.js"; + +export class AppendMenu { + constructor(textArea, onContentChange) { + this.textArea = textArea; + this.onContentChange = onContentChange; + this.container = null; + this.button = null; + this.submenu = null; + this.isDisabled = true; + } + + createMenuElements() { + // Append submenu container + this.container = document.createElement('div'); + this.container.className = 'notes-submenu-container'; + + this.button = document.createElement('button'); + this.button.innerHTML = ''; + this.button.className = 'notes-btn append-loras'; + this.button.title = 'Append'; + this.button.onclick = () => this.toggleMenu(); + this.button.disabled = this.isDisabled; + + // Create submenu + this.submenu = document.createElement('div'); + this.submenu.className = 'notes-submenu'; + this.submenu.style.display = 'none'; + + // Create submenu items + const rawOption = document.createElement('div'); + rawOption.className = 'notes-submenu-item'; + rawOption.textContent = 'Append raw LoRA filenames list'; + rawOption.onclick = () => this.selectOption('raw'); + + const tagOption = document.createElement('div'); + tagOption.className = 'notes-submenu-item'; + tagOption.textContent = "Append LoRAs in '' format"; + tagOption.onclick = () => this.selectOption('tag'); + + const metaOption = document.createElement('div'); + metaOption.className = 'notes-submenu-item'; + metaOption.textContent = "Append LoRAs in ', metadata' format"; + metaOption.onclick = () => this.selectOption('meta'); + + this.submenu.appendChild(rawOption); + this.submenu.appendChild(tagOption); + this.submenu.appendChild(metaOption); + + this.container.appendChild(this.button); + this.container.appendChild(this.submenu); + + return this.container; + } + + setDisabled(disabled) { + this.isDisabled = disabled; + if (this.button) { + this.button.disabled = disabled; + } + } + + toggleMenu() { + if (this.submenu.style.display === 'none') { + this.submenu.style.display = 'block'; + // Add click listener to close menu when clicking outside + document.addEventListener('click', this.closeMenuHandler); + } else { + this.submenu.style.display = 'none'; + document.removeEventListener('click', this.closeMenuHandler); + } + } + + closeMenuHandler = (event) => { + if (!this.container.contains(event.target)) { + this.submenu.style.display = 'none'; + document.removeEventListener('click', this.closeMenuHandler); + } + } + + selectOption(type) { + this.submenu.style.display = 'none'; + document.removeEventListener('click', this.closeMenuHandler); + + switch(type) { + case 'raw': + this.appendLorasList(); + break; + case 'tag': + this.appendLorasTags(); + break; + case 'meta': + this.appendLorasWithMetadata(); + break; + } + } + + async appendLorasList() { + try { + const loras = await NotesApi.getLorasList(); + + // Normalize backslashes to forward slashes + const normalizedLoras = loras.map(lora => lora.replace(/\\/g, '/')); + const lorasText = normalizedLoras.join('\n'); + + this.insertTextIntoTextarea(lorasText); + } catch (error) { + console.error('Error appending LoRAs:', error); + alert('Failed to append LoRAs list'); + } + } + + async appendLorasTags() { + try { + const loras = await NotesApi.getLorasList(); + + // Format each LoRA as , stripping .safetensors extension and normalizing slashes + const lorasTags = loras.map(lora => { + const nameWithoutExt = lora.replace(/\.safetensors$/i, ''); + const normalizedPath = nameWithoutExt.replace(/\\/g, '/'); + return ``; + }); + const lorasText = lorasTags.join('\n'); + + this.insertTextIntoTextarea(lorasText); + } catch (error) { + console.error('Error appending LoRAs tags:', error); + alert('Failed to append LoRAs tags'); + } + } + + async appendLorasWithMetadata() { + try { + const loras = await NotesApi.getLorasWithMetadata(); + + // Format each LoRA with metadata as , metadata + const lorasWithMeta = loras.map(lora => { + const nameWithoutExt = lora.name.replace(/\.safetensors$/i, ''); + const normalizedPath = nameWithoutExt.replace(/\\/g, '/'); + const baseTag = ``; + + if (lora.metadata) { + return `${baseTag}, ${lora.metadata}`; + } else { + return baseTag; + } + }); + const lorasText = lorasWithMeta.join('\n'); + + this.insertTextIntoTextarea(lorasText); + } catch (error) { + console.error('Error appending LoRAs with metadata:', error); + alert('Failed to append LoRAs with metadata'); + } + } + + insertTextIntoTextarea(text) { + if (!this.textArea) return; + + // Add newline before appending if current content doesn't end with one + const separator = this.textArea.value && !this.textArea.value.endsWith('\n') ? '\n' : ''; + const textToInsert = separator + text; + + // Focus textarea to ensure it's active + this.textArea.focus(); + + // Move cursor to end + this.textArea.setSelectionRange(this.textArea.value.length, this.textArea.value.length); + + // Use document.execCommand to preserve undo history + document.execCommand('insertText', false, textToInsert); + + // Trigger change handling + if (this.onContentChange) { + this.onContentChange(); + } + } +} \ No newline at end of file diff --git a/js/extension.js b/js/extension.js index b4d13d3..cbd413f 100644 --- a/js/extension.js +++ b/js/extension.js @@ -1,4 +1,8 @@ import { app } from "/scripts/app.js"; +import { NotesStore } from "./stores/NotesStore.js"; +import { AppendMenu } from "./components/AppendMenu.js"; +import { NotesApi } from "./api/notesApi.js"; +import { isValidFilename } from "./utils/validation.js"; // Load CSS const link = document.createElement('link'); @@ -6,99 +10,6 @@ link.rel = 'stylesheet'; link.href = '/extensions/comfyui-notes-sidebar/notes-sidebar.css'; document.head.appendChild(link); -class NotesStore extends EventTarget { - constructor() { - super(); - this.state = { - notes: [], - activeNote: null, - unsavedChanges: new Map(), // noteName -> boolean - noteContent: new Map(), // noteName -> string (current content) - savedContent: new Map() // noteName -> string (last saved content) - }; - } - - getState() { - return { ...this.state }; - } - - setState(updates) { - const prevState = { ...this.state }; - this.state = { ...this.state, ...updates }; - - // Emit state change event - this.dispatchEvent(new CustomEvent('stateChanged', { - detail: { - prevState, - newState: this.getState(), - updates - } - })); - } - - // Convenience methods for common state operations - setNotes(notes) { - this.setState({ notes }); - } - - setActiveNote(activeNote) { - this.setState({ activeNote }); - } - - setNoteContent(noteName, content) { - const noteContent = new Map(this.state.noteContent); - noteContent.set(noteName, content); - this.setState({ noteContent }); - } - - setSavedContent(noteName, content) { - const savedContent = new Map(this.state.savedContent); - savedContent.set(noteName, content); - this.setState({ savedContent }); - } - - setUnsavedChanges(noteName, hasChanges) { - const unsavedChanges = new Map(this.state.unsavedChanges); - unsavedChanges.set(noteName, hasChanges); - this.setState({ unsavedChanges }); - } - - deleteNoteFromState(noteName) { - const unsavedChanges = new Map(this.state.unsavedChanges); - const noteContent = new Map(this.state.noteContent); - const savedContent = new Map(this.state.savedContent); - - unsavedChanges.delete(noteName); - noteContent.delete(noteName); - savedContent.delete(noteName); - - this.setState({ unsavedChanges, noteContent, savedContent }); - } - - renameNoteInState(oldName, newName) { - const unsavedChanges = new Map(this.state.unsavedChanges); - const noteContent = new Map(this.state.noteContent); - const savedContent = new Map(this.state.savedContent); - - // Transfer state from old name to new name - const unsavedState = unsavedChanges.get(oldName) || false; - const content = noteContent.get(oldName) || ''; - const saved = savedContent.get(oldName) || ''; - - // Remove old - unsavedChanges.delete(oldName); - noteContent.delete(oldName); - savedContent.delete(oldName); - - // Add new - unsavedChanges.set(newName, unsavedState); - noteContent.set(newName, content); - savedContent.set(newName, saved); - - this.setState({ unsavedChanges, noteContent, savedContent }); - } -} - class NotesSidebar { constructor() { this.store = new NotesStore(); @@ -108,9 +19,7 @@ class NotesSidebar { this.saveBtn = null; this.renameBtn = null; this.deleteBtn = null; - this.appendLorasBtn = null; - this.appendLorasTagsBtn = null; - this.appendLorasMetaBtn = null; + this.appendMenu = null; this.isInitialized = false; this.sidebarElement = null; @@ -173,7 +82,7 @@ class NotesSidebar { } updateButtonState() { - if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendLorasBtn) return; + if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendMenu) return; const hasActiveNote = !!this.activeNote; const hasUnsavedChanges = hasActiveNote && this.unsavedChanges.get(this.activeNote); @@ -184,7 +93,7 @@ class NotesSidebar { // Rename, delete, and append buttons: enabled only when there's an active note this.renameBtn.disabled = !hasActiveNote; this.deleteBtn.disabled = !hasActiveNote; - this.appendLorasBtn.disabled = !hasActiveNote; + this.appendMenu.setDisabled(!hasActiveNote); } async init() { @@ -288,48 +197,9 @@ 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'; - this.appendLorasBtn.onclick = () => this.toggleLoraMenu(); - this.appendLorasBtn.disabled = true; - - // Create submenu - this.loraSubmenu = document.createElement('div'); - this.loraSubmenu.className = 'notes-submenu'; - this.loraSubmenu.style.display = 'none'; - - // 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.loraMenuContainer); + // Append menu will be added later after textarea is created rightButtonContainer.appendChild(this.deleteBtn); buttonContainer.appendChild(leftButtonContainer); @@ -352,15 +222,22 @@ class NotesSidebar { // Add event listener to track changes this.textArea.addEventListener('input', () => this.onTextAreaChange()); + // Initialize AppendMenu with textarea reference and change handler + this.appendMenu = new AppendMenu(this.textArea, () => this.onTextAreaChange()); + + // Add append menu to the left button container + const leftButtonContainer = this.sidebarElement.querySelector('.notes-button-group'); + if (leftButtonContainer) { + leftButtonContainer.appendChild(this.appendMenu.createMenuElements()); + } + this.contentArea.appendChild(this.textArea); parent.appendChild(this.contentArea); } async loadNotes() { try { - const response = await fetch('/notes_sidebar/list'); - const data = await response.json(); - const notes = data.notes || []; + const notes = await NotesApi.getNotesList(); this.store.setNotes(notes); @@ -416,9 +293,7 @@ class NotesSidebar { return; } else { // Load from server - const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`); - const data = await response.json(); - const content = data.content || ''; + const content = await NotesApi.getNoteContent(noteName); // Store both current and saved content this.store.setNoteContent(noteName, content); @@ -432,21 +307,13 @@ class NotesSidebar { async saveActiveNote() { try { const content = this.textArea.value; - const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ content: content }) - }); + await NotesApi.saveNote(this.activeNote, content); - if (response.ok) { - console.log('Note saved successfully'); - // Mark as saved - this.store.setSavedContent(this.activeNote, content); - this.store.setNoteContent(this.activeNote, content); - this.store.setUnsavedChanges(this.activeNote, false); - } else { - console.error('Error saving note'); - } + console.log('Note saved successfully'); + // Mark as saved + this.store.setSavedContent(this.activeNote, content); + this.store.setNoteContent(this.activeNote, content); + this.store.setUnsavedChanges(this.activeNote, false); } catch (error) { console.error('Error saving note:', error); } @@ -456,19 +323,17 @@ class NotesSidebar { if (!confirm(`Are you sure you want to delete "${this.activeNote}"?`)) return; try { - const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, { method: 'DELETE' }); + await NotesApi.deleteNote(this.activeNote); - if (response.ok) { - // Clean up tracking for deleted note - const deletedNote = this.activeNote; + // Clean up tracking for deleted note + const deletedNote = this.activeNote; - // Clear active note first to ensure state change is triggered - this.store.setActiveNote(null); - this.store.deleteNoteFromState(deletedNote); + // Clear active note first to ensure state change is triggered + this.store.setActiveNote(null); + this.store.deleteNoteFromState(deletedNote); - // Refresh notes list (this will handle setting activeNote correctly) - await this.refreshNotes(); - } + // Refresh notes list (this will handle setting activeNote correctly) + await this.refreshNotes(); } catch (error) { console.error('Error deleting note:', error); } @@ -498,28 +363,22 @@ class NotesSidebar { const currentContent = this.textArea.value; // Create new note with the content - const createResponse = await fetch(`/notes_sidebar/note/${encodeURIComponent(newName)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ content: currentContent }) - }); + await NotesApi.saveNote(newName, currentContent); - if (createResponse.ok) { - // Delete old note - await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, { method: 'DELETE' }); + // Delete old note + await NotesApi.deleteNote(this.activeNote); - // Update tracking maps through store - const oldName = this.activeNote; - this.store.renameNoteInState(oldName, newName); + // Update tracking maps through store + const oldName = this.activeNote; + this.store.renameNoteInState(oldName, newName); - // Set saved content to what we just saved - this.store.setSavedContent(newName, currentContent); - this.store.setUnsavedChanges(newName, false); + // Set saved content to what we just saved + this.store.setSavedContent(newName, currentContent); + this.store.setUnsavedChanges(newName, false); - // Update active note and refresh - this.store.setActiveNote(newName); - await this.refreshNotes(); - } + // Update active note and refresh + this.store.setActiveNote(newName); + await this.refreshNotes(); } catch (error) { console.error('Error renaming note:', error); } @@ -552,21 +411,15 @@ class NotesSidebar { } try { - const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ content: '' }) - }); + await NotesApi.saveNote(noteName, ''); - if (response.ok) { - await this.refreshNotes(); - this.store.setActiveNote(noteName); + await this.refreshNotes(); + this.store.setActiveNote(noteName); - // Initialize tracking for new note - this.store.setNoteContent(noteName, ''); - this.store.setSavedContent(noteName, ''); - this.store.setUnsavedChanges(noteName, false); - } + // Initialize tracking for new note + this.store.setNoteContent(noteName, ''); + this.store.setSavedContent(noteName, ''); + this.store.setUnsavedChanges(noteName, false); } catch (error) { console.error('Error creating note:', error); } @@ -593,171 +446,8 @@ 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'); - const data = await response.json(); - - if (response.ok && data.loras) { - // Normalize backslashes to forward slashes - const normalizedLoras = data.loras.map(lora => lora.replace(/\\/g, '/')); - const lorasText = normalizedLoras.join('\n'); - - // Add newline before appending if current content doesn't end with one - const separator = this.textArea.value && !this.textArea.value.endsWith('\n') ? '\n' : ''; - const textToInsert = separator + lorasText; - - // Focus textarea to ensure it's active - this.textArea.focus(); - - // Move cursor to end - this.textArea.setSelectionRange(this.textArea.value.length, this.textArea.value.length); - - // Use document.execCommand to preserve undo history - document.execCommand('insertText', false, textToInsert); - - // Trigger change handling - this.onTextAreaChange(); - } else { - console.error('Error fetching LoRAs:', data.error || 'Unknown error'); - alert('Failed to fetch LoRAs list'); - } - } catch (error) { - console.error('Error appending LoRAs:', error); - alert('Failed to append LoRAs list'); - } - } - - async appendLorasTags() { - try { - const response = await fetch('/notes_sidebar/loras'); - const data = await response.json(); - - if (response.ok && data.loras) { - // Format each LoRA as , stripping .safetensors extension and normalizing slashes - const lorasTags = data.loras.map(lora => { - const nameWithoutExt = lora.replace(/\.safetensors$/i, ''); - const normalizedPath = nameWithoutExt.replace(/\\/g, '/'); - return ``; - }); - const lorasText = lorasTags.join('\n'); - - // Add newline before appending if current content doesn't end with one - const separator = this.textArea.value && !this.textArea.value.endsWith('\n') ? '\n' : ''; - const textToInsert = separator + lorasText; - - // Focus textarea to ensure it's active - this.textArea.focus(); - - // Move cursor to end - this.textArea.setSelectionRange(this.textArea.value.length, this.textArea.value.length); - - // Use document.execCommand to preserve undo history - document.execCommand('insertText', false, textToInsert); - - // Trigger change handling - this.onTextAreaChange(); - } else { - console.error('Error fetching LoRAs:', data.error || 'Unknown error'); - alert('Failed to fetch LoRAs list'); - } - } catch (error) { - console.error('Error appending LoRAs tags:', error); - alert('Failed to append LoRAs tags'); - } - } - - async appendLorasWithMetadata() { - try { - const response = await fetch('/notes_sidebar/loras_with_metadata'); - const data = await response.json(); - - if (response.ok && data.loras) { - // Format each LoRA with metadata as , metadata - const lorasWithMeta = data.loras.map(lora => { - const nameWithoutExt = lora.name.replace(/\.safetensors$/i, ''); - const normalizedPath = nameWithoutExt.replace(/\\/g, '/'); - const baseTag = ``; - - if (lora.metadata) { - return `${baseTag}, ${lora.metadata}`; - } else { - return baseTag; - } - }); - const lorasText = lorasWithMeta.join('\n'); - - // Add newline before appending if current content doesn't end with one - const separator = this.textArea.value && !this.textArea.value.endsWith('\n') ? '\n' : ''; - const textToInsert = separator + lorasText; - - // Focus textarea to ensure it's active - this.textArea.focus(); - - // Move cursor to end - this.textArea.setSelectionRange(this.textArea.value.length, this.textArea.value.length); - - // Use document.execCommand to preserve undo history - document.execCommand('insertText', false, textToInsert); - - // Trigger change handling - this.onTextAreaChange(); - } else { - console.error('Error fetching LoRAs with metadata:', data.error || 'Unknown error'); - alert('Failed to fetch LoRAs with metadata'); - } - } catch (error) { - console.error('Error appending LoRAs with metadata:', error); - alert('Failed to append LoRAs with metadata'); - } - } } -// Helper function to validate filename safety -function isValidFilename(filename) { - if (!filename || filename.trim() === '') return false; - - // Check for unsafe characters: / \\ : * ? " < > | - const unsafeChars = /[/\\:*?"<>|]/; - if (unsafeChars.test(filename)) return false; - - return true; -} const notesSidebar = new NotesSidebar(); diff --git a/js/stores/NotesStore.js b/js/stores/NotesStore.js new file mode 100644 index 0000000..b8263d4 --- /dev/null +++ b/js/stores/NotesStore.js @@ -0,0 +1,92 @@ +export class NotesStore extends EventTarget { + constructor() { + super(); + this.state = { + notes: [], + activeNote: null, + unsavedChanges: new Map(), // noteName -> boolean + noteContent: new Map(), // noteName -> string (current content) + savedContent: new Map() // noteName -> string (last saved content) + }; + } + + getState() { + return { ...this.state }; + } + + setState(updates) { + const prevState = { ...this.state }; + this.state = { ...this.state, ...updates }; + + // Emit state change event + this.dispatchEvent(new CustomEvent('stateChanged', { + detail: { + prevState, + newState: this.getState(), + updates + } + })); + } + + // Convenience methods for common state operations + setNotes(notes) { + this.setState({ notes }); + } + + setActiveNote(activeNote) { + this.setState({ activeNote }); + } + + setNoteContent(noteName, content) { + const noteContent = new Map(this.state.noteContent); + noteContent.set(noteName, content); + this.setState({ noteContent }); + } + + setSavedContent(noteName, content) { + const savedContent = new Map(this.state.savedContent); + savedContent.set(noteName, content); + this.setState({ savedContent }); + } + + setUnsavedChanges(noteName, hasChanges) { + const unsavedChanges = new Map(this.state.unsavedChanges); + unsavedChanges.set(noteName, hasChanges); + this.setState({ unsavedChanges }); + } + + deleteNoteFromState(noteName) { + const unsavedChanges = new Map(this.state.unsavedChanges); + const noteContent = new Map(this.state.noteContent); + const savedContent = new Map(this.state.savedContent); + + unsavedChanges.delete(noteName); + noteContent.delete(noteName); + savedContent.delete(noteName); + + this.setState({ unsavedChanges, noteContent, savedContent }); + } + + renameNoteInState(oldName, newName) { + const unsavedChanges = new Map(this.state.unsavedChanges); + const noteContent = new Map(this.state.noteContent); + const savedContent = new Map(this.state.savedContent); + + // Transfer state from old name to new name + const unsavedState = unsavedChanges.get(oldName) || false; + const content = noteContent.get(oldName) || ''; + const saved = savedContent.get(oldName) || ''; + + // Remove old + unsavedChanges.delete(oldName); + noteContent.delete(oldName); + savedContent.delete(oldName); + + // Add new + unsavedChanges.set(newName, unsavedState); + noteContent.set(newName, content); + savedContent.set(newName, saved); + + this.setState({ unsavedChanges, noteContent, savedContent }); + } +} \ No newline at end of file diff --git a/js/utils/validation.js b/js/utils/validation.js new file mode 100644 index 0000000..c7a3a58 --- /dev/null +++ b/js/utils/validation.js @@ -0,0 +1,10 @@ +// Helper function to validate filename safety +export function isValidFilename(filename) { + if (!filename || filename.trim() === '') return false; + + // Check for unsafe characters: / \\ : * ? " < > | + const unsafeChars = /[/\\:*?"<>|]/; + if (unsafeChars.test(filename)) return false; + + return true; +} \ No newline at end of file