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