Files
jfcantu-ComfyUI-AutoNotes/web/autonotes.js
T
2025-09-29 23:05:02 -07:00

2983 lines
103 KiB
JavaScript
Raw Blame History

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