Files

4486 lines
160 KiB
JavaScript
Raw Permalink 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.expandedNotes = new Set(); // Track which notes are expanded
this.lastSelectedNodeType = null; // Track last selected node to detect changes
this.init();
}
async init() {
// Wait for app to be ready
await new Promise(resolve => {
if (app.canvas) {
resolve();
} else {
app.registerExtension({
name: "AutoNotes.Init",
setup() {
resolve();
}
});
}
});
// Load marked library
await this.loadMarkedLibrary();
// Load node definitions
await this.loadNodeTypes();
// Try to use ComfyUI's built-in sidebar tab API, fall back to legacy sidebar
if (app.extensionManager?.registerSidebarTab) {
this.registerAsSidebarTab();
} else {
this.createLegacySidebar();
}
this.setupEventListeners();
await this.refreshNotes();
}
async loadMarkedLibrary() {
// Load the marked library using script tag since it's UMD format
if (typeof window.marked !== 'undefined') {
this.marked = window.marked;
return;
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = '/extensions/ComfyUI-AutoNotes/marked.min.js';
script.onload = () => {
this.marked = window.marked;
resolve();
};
script.onerror = () => {
console.error('Failed to load marked library');
this.marked = null;
resolve(); // Resolve anyway, will use fallback
};
document.head.appendChild(script);
});
}
async loadNodeTypes() {
try {
const response = await api.fetchApi('/object_info');
this.nodeTypes = await response.json();
} catch (error) {
console.error('Failed to load node types:', error);
this.nodeTypes = {};
}
}
registerAsSidebarTab() {
app.extensionManager.registerSidebarTab({
id: 'autonotes',
icon: 'pi pi-book',
title: 'AutoNotes',
tooltip: 'AutoNotes - Workflow Documentation',
type: 'custom',
render: (container) => {
this.buildSidebarContent(container);
}
});
}
createLegacySidebar() {
this.sidebar = document.createElement('div');
this.sidebar.id = 'autonotes-sidebar';
this.sidebar.style.cssText = `
position: fixed;
right: 0;
top: 0;
width: 300px;
height: 100vh;
background: var(--comfy-menu-bg, #2a2a2a);
border-left: 1px solid var(--border-color, #555);
z-index: 1000;
display: flex;
flex-direction: column;
box-sizing: border-box;
font-family: Arial, sans-serif;
color: var(--fg-color, #fff);
`;
this.buildSidebarContent(this.sidebar);
document.body.appendChild(this.sidebar);
}
buildSidebarContent(container) {
container.innerHTML = '';
// Main wrapper
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
box-sizing: border-box;
font-family: Arial, sans-serif;
color: var(--fg-color, #fff);
`;
// Control panel
const controlPanel = document.createElement('div');
controlPanel.style.cssText = `
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color, #555);
flex-shrink: 0;
`;
// Display mode selector
const modeContainer = document.createElement('div');
modeContainer.style.marginBottom = '10px';
const modeLabel = document.createElement('label');
modeLabel.textContent = 'Display: ';
modeLabel.style.marginRight = '10px';
const modeSelect = document.createElement('select');
modeSelect.style.cssText = `
background: var(--comfy-input-bg, #333);
color: var(--fg-color, #fff);
border: 1px solid var(--border-color, #555);
padding: 5px;
`;
modeSelect.innerHTML = `
<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);
// Tag filter section
const tagFilterContainer = document.createElement('div');
tagFilterContainer.style.marginBottom = '10px';
const tagFilterLabel = document.createElement('label');
tagFilterLabel.textContent = 'Filter by Tags: ';
tagFilterLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
this.tagFilterSelect = document.createElement('div');
this.tagFilterSelect.style.cssText = `
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #555);
padding: 5px;
border-radius: 4px;
min-height: 30px;
cursor: pointer;
position: relative;
`;
this.tagFilterSelect.textContent = 'Click to select tags...';
this.selectedTagFilters = new Set();
this.tagFilterSelect.addEventListener('click', (e) => {
e.stopPropagation();
this.showTagFilterDropdown();
});
tagFilterContainer.appendChild(tagFilterLabel);
tagFilterContainer.appendChild(this.tagFilterSelect);
// Buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
buttonContainer.style.flexWrap = 'wrap';
const addButton = document.createElement('button');
addButton.textContent = 'Add';
addButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
addButton.addEventListener('click', () => this.addNote());
const addFromCurrentButton = document.createElement('button');
addFromCurrentButton.textContent = 'Add From Current Node';
addFromCurrentButton.style.cssText = `
background: #00a000;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
flex: 1;
`;
addFromCurrentButton.addEventListener('click', () => this.addNoteFromCurrentNode());
const editButton = document.createElement('button');
editButton.textContent = 'Edit';
editButton.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
editButton.addEventListener('click', () => this.openEditDialog());
buttonContainer.appendChild(addButton);
buttonContainer.appendChild(addFromCurrentButton);
buttonContainer.appendChild(editButton);
controlPanel.appendChild(modeContainer);
controlPanel.appendChild(tagFilterContainer);
controlPanel.appendChild(buttonContainer);
// Notes display area
this.notesContainer = document.createElement('div');
this.notesContainer.style.cssText = `
flex: 1;
overflow-y: auto;
padding-right: 5px;
`;
wrapper.appendChild(controlPanel);
wrapper.appendChild(this.notesContainer);
container.appendChild(wrapper);
// Render any already-loaded notes (tab may open after init fetch)
if (this.notes.length > 0) {
this.renderNotes();
}
}
setupEventListeners() {
// Use polling to check for selection changes (more reliable)
this.lastSelectionCheck = null;
setInterval(() => {
if (app.canvas && app.canvas.selected_nodes) {
const selectedNodeIds = Object.keys(app.canvas.selected_nodes);
const currentSelection = selectedNodeIds.join(',');
// Only update if selection changed
if (currentSelection !== this.lastSelectionCheck) {
this.lastSelectionCheck = currentSelection;
this.updateSelectedNode();
}
}
}, 300); // Check every 300ms
// Listen for workflow changes
api.addEventListener("graphChanged", () => {
this.updateWorkflowName();
});
}
updateSelectedNode() {
// Try multiple methods to get selected nodes
let selectedNodes = app.canvas?.selected_nodes;
// Alternative: check graph canvas
if (!selectedNodes && app.graph?.list_of_graphcanvas?.[0]) {
selectedNodes = app.graph.list_of_graphcanvas[0].selected_nodes;
}
let currentNodeType = null;
// Check if it's an object with keys (not a Set)
const selectedNodeIds = selectedNodes ? Object.keys(selectedNodes) : [];
if (selectedNodeIds.length > 0) {
// Get the first selected node
const nodeId = selectedNodeIds[0];
const node = selectedNodes[nodeId];
if (node && node.type) {
currentNodeType = node.type;
this.selectedNodeType = node.type;
// Collect node attributes (widgets/inputs)
this.selectedNodeAttributes = {};
if (node.widgets) {
for (const widget of node.widgets) {
this.selectedNodeAttributes[widget.name] = widget.value;
}
}
}
} else {
this.selectedNodeType = null;
this.selectedNodeAttributes = {};
}
// Only refresh if selection changed and in automatic mode
if (this.displayMode === "automatic" && currentNodeType !== this.lastSelectedNodeType) {
this.lastSelectedNodeType = currentNodeType;
this.refreshNotes();
}
}
updateWorkflowName() {
// Try to get workflow name from various sources
this.workflowName = app.ui.lastQueueSize || document.title || "";
if (this.displayMode === "automatic") {
this.refreshNotes();
}
}
async refreshNotes() {
try {
const params = new URLSearchParams({
mode: this.displayMode
});
if (this.selectedNodeType) {
params.append('node_type', this.selectedNodeType);
}
if (Object.keys(this.selectedNodeAttributes).length > 0) {
params.append('node_attributes', JSON.stringify(this.selectedNodeAttributes));
}
if (this.workflowName) {
params.append('workflow_name', this.workflowName);
}
// Collect workflow node information for node_in_workflow and attribute_in_workflow triggers
const workflowNodes = {};
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
// For each node type in workflow, collect first instance's attributes
if (!workflowNodes[node.type]) {
const attributes = {};
if (node.properties) {
Object.assign(attributes, node.properties);
}
if (node.widgets) {
for (const widget of node.widgets) {
attributes[widget.name] = widget.value;
}
}
workflowNodes[node.type] = attributes;
}
}
}
if (Object.keys(workflowNodes).length > 0) {
params.append('workflow_nodes', JSON.stringify(workflowNodes));
}
const response = await api.fetchApi(`/autonotes/notes?${params}`);
this.notes = await response.json();
this.renderNotes();
} catch (error) {
console.error('Failed to fetch notes:', error);
}
}
renderNotes() {
if (!this.notesContainer) return;
this.notesContainer.innerHTML = '';
// Apply tag filtering
let filteredNotes = this.notes;
if (this.selectedTagFilters.size > 0) {
filteredNotes = this.notes.filter(note => {
const noteTags = note.tags || [];
// Note must have ALL selected tags (AND logic)
return Array.from(this.selectedTagFilters).every(filterTag =>
noteTags.includes(filterTag)
);
});
}
if (filteredNotes.length === 0) {
const emptyMessage = document.createElement('div');
emptyMessage.textContent = this.selectedTagFilters.size > 0 ?
'No notes match the selected tags' : 'No notes to display';
emptyMessage.style.cssText = `
text-align: center;
color: #888;
font-style: italic;
margin-top: 20px;
`;
this.notesContainer.appendChild(emptyMessage);
return;
}
for (const note of filteredNotes) {
this.renderNote(note);
}
}
showTagFilterDropdown() {
// Collect all unique tags from all notes
const allTags = new Set();
for (const note of this.notes) {
if (note.tags) {
note.tags.forEach(tag => allTags.add(tag));
}
}
if (allTags.size === 0) {
alert('No tags found. Add tags to notes first.');
return;
}
// Remove existing dropdown
const existingDropdown = document.querySelector('.tag-filter-dropdown');
if (existingDropdown) {
existingDropdown.remove();
return;
}
// Create dropdown
const dropdown = document.createElement('div');
dropdown.className = 'tag-filter-dropdown';
dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 4px;
margin-top: 2px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
`;
// Add "Clear All" option
const clearOption = document.createElement('div');
clearOption.textContent = 'Clear All Filters';
clearOption.style.cssText = `
padding: 8px 10px;
cursor: pointer;
color: #f88;
border-bottom: 1px solid #555;
`;
clearOption.addEventListener('mouseenter', () => {
clearOption.style.background = '#333';
});
clearOption.addEventListener('mouseleave', () => {
clearOption.style.background = '';
});
clearOption.addEventListener('click', () => {
this.selectedTagFilters.clear();
this.updateTagFilterDisplay();
this.refreshNotes();
dropdown.remove();
});
dropdown.appendChild(clearOption);
// Add tag options
Array.from(allTags).sort().forEach(tag => {
const option = document.createElement('div');
option.style.cssText = `
padding: 8px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = this.selectedTagFilters.has(tag);
const label = document.createElement('span');
label.textContent = tag;
option.addEventListener('mouseenter', () => {
option.style.background = '#333';
});
option.addEventListener('mouseleave', () => {
option.style.background = '';
});
option.addEventListener('click', () => {
if (this.selectedTagFilters.has(tag)) {
this.selectedTagFilters.delete(tag);
checkbox.checked = false;
} else {
this.selectedTagFilters.add(tag);
checkbox.checked = true;
}
this.updateTagFilterDisplay();
this.refreshNotes();
});
option.appendChild(checkbox);
option.appendChild(label);
dropdown.appendChild(option);
});
this.tagFilterSelect.style.position = 'relative';
this.tagFilterSelect.appendChild(dropdown);
// Close dropdown when clicking outside
const closeDropdown = (e) => {
if (!this.tagFilterSelect.contains(e.target)) {
dropdown.remove();
document.removeEventListener('click', closeDropdown);
}
};
setTimeout(() => {
document.addEventListener('click', closeDropdown);
}, 0);
}
updateTagFilterDisplay() {
if (this.selectedTagFilters.size === 0) {
this.tagFilterSelect.innerHTML = 'Click to select tags...';
this.tagFilterSelect.style.color = '';
} else {
this.tagFilterSelect.innerHTML = '';
this.tagFilterSelect.style.color = '#fff';
this.tagFilterSelect.style.cssText += `
display: flex;
flex-wrap: wrap;
gap: 5px;
`;
Array.from(this.selectedTagFilters).forEach(tag => {
const tagBadge = document.createElement('span');
tagBadge.style.cssText = `
background: #007acc;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
display: inline-block;
`;
tagBadge.textContent = tag;
this.tagFilterSelect.appendChild(tagBadge);
});
// Re-add base styles
this.tagFilterSelect.style.background = '#333';
this.tagFilterSelect.style.border = '1px solid #555';
this.tagFilterSelect.style.padding = '5px';
this.tagFilterSelect.style.borderRadius = '4px';
this.tagFilterSelect.style.minHeight = '30px';
this.tagFilterSelect.style.cursor = 'pointer';
this.tagFilterSelect.style.position = 'relative';
}
}
renderNote(note) {
const noteElement = document.createElement('div');
noteElement.style.cssText = `
margin-bottom: 15px;
border: 1px solid #555;
border-radius: 4px;
overflow: hidden;
`;
// Note header (collapsible)
const header = document.createElement('div');
header.style.cssText = `
background: #333;
padding: 10px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
`;
const title = document.createElement('span');
title.textContent = note.name || 'Untitled Note';
title.style.fontWeight = 'bold';
const controls = document.createElement('div');
controls.style.display = 'flex';
controls.style.gap = '5px';
controls.style.alignItems = 'center';
// Pin button
const pinButton = document.createElement('button');
pinButton.textContent = note.pinned ? '📌' : '📍';
pinButton.title = note.pinned ? 'Unpin' : 'Pin';
pinButton.style.cssText = `
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 0;
font-size: 16px;
opacity: ${note.pinned ? '1' : '0.5'};
`;
pinButton.addEventListener('click', async (e) => {
e.stopPropagation();
await this.togglePin(note);
});
controls.appendChild(pinButton);
// Collapse indicator
const collapseIcon = document.createElement('span');
collapseIcon.textContent = '▼';
collapseIcon.style.transition = 'transform 0.2s';
controls.appendChild(collapseIcon);
header.appendChild(title);
header.appendChild(controls);
// Content container
const contentContainer = document.createElement('div');
contentContainer.style.cssText = `
background: #2a2a2a;
border-top: 1px solid #555;
`;
// Tags section (at the top)
const tagsSection = document.createElement('div');
tagsSection.style.cssText = `
padding: 10px;
border-bottom: 1px solid #555;
background: #252525;
`;
const tagsHeader = document.createElement('div');
tagsHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`;
const tagsLabel = document.createElement('span');
tagsLabel.textContent = 'Tags:';
tagsLabel.style.cssText = `
font-size: 11px;
color: #aaa;
`;
const addTagBtn = document.createElement('button');
addTagBtn.textContent = '+ Tag';
addTagBtn.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 2px 6px;
cursor: pointer;
border-radius: 3px;
font-size: 10px;
`;
addTagBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const tag = prompt('Enter tag name:');
if (tag && tag.trim()) {
const trimmedTag = tag.trim();
if (!note.tags) note.tags = [];
if (!note.tags.includes(trimmedTag)) {
note.tags.push(trimmedTag);
await this.updateNoteTags(note);
this.renderNotes();
}
}
});
tagsHeader.appendChild(tagsLabel);
tagsHeader.appendChild(addTagBtn);
const tagsContainer = document.createElement('div');
tagsContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 5px;
min-height: 20px;
`;
if (note.tags && note.tags.length > 0) {
note.tags.forEach(tag => {
const tagBadge = document.createElement('div');
tagBadge.style.cssText = `
background: #007acc;
color: white;
padding: 3px 6px;
border-radius: 3px;
font-size: 11px;
display: flex;
align-items: center;
gap: 4px;
`;
const tagText = document.createElement('span');
tagText.textContent = tag;
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.cssText = `
background: transparent;
border: none;
color: white;
cursor: pointer;
font-size: 14px;
padding: 0;
line-height: 1;
`;
removeBtn.addEventListener('click', async (e) => {
e.stopPropagation();
note.tags = note.tags.filter(t => t !== tag);
await this.updateNoteTags(note);
this.renderNotes();
});
tagBadge.appendChild(tagText);
tagBadge.appendChild(removeBtn);
tagsContainer.appendChild(tagBadge);
});
} else {
const noTags = document.createElement('span');
noTags.textContent = 'No tags';
noTags.style.cssText = `
font-size: 11px;
color: #666;
font-style: italic;
`;
tagsContainer.appendChild(noTags);
}
tagsSection.appendChild(tagsHeader);
tagsSection.appendChild(tagsContainer);
contentContainer.appendChild(tagsSection);
// Mode toggle (for markdown notes)
let isEditMode = false;
let modeToggle = null;
if (note.format_style === 'markdown') {
const toggleBar = document.createElement('div');
toggleBar.style.cssText = `
padding: 5px 10px;
background: #252525;
border-bottom: 1px solid #555;
display: flex;
gap: 5px;
`;
const viewBtn = document.createElement('button');
viewBtn.textContent = 'View';
viewBtn.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 3px 10px;
cursor: pointer;
border-radius: 3px;
font-size: 11px;
`;
const editBtn = document.createElement('button');
editBtn.textContent = 'Edit';
editBtn.style.cssText = `
background: #555;
color: white;
border: none;
padding: 3px 10px;
cursor: pointer;
border-radius: 3px;
font-size: 11px;
`;
toggleBar.appendChild(viewBtn);
toggleBar.appendChild(editBtn);
contentContainer.appendChild(toggleBar);
modeToggle = { viewBtn, editBtn };
}
// View content (rendered markdown or plain text) - resizable
const viewContent = document.createElement('div');
viewContent.style.cssText = `
padding: 10px;
min-height: 50px;
max-height: none;
overflow-y: auto;
position: relative;
resize: vertical;
`;
if (note.format_style === 'markdown') {
viewContent.innerHTML = this.renderMarkdown(note.content);
} else {
viewContent.textContent = note.content;
viewContent.style.whiteSpace = 'pre-wrap';
}
// Edit content (textarea)
const editContent = document.createElement('textarea');
editContent.value = note.content;
editContent.style.cssText = `
width: 100%;
min-height: 100px;
padding: 10px;
background: #1a1a1a;
color: #fff;
border: none;
font-family: monospace;
font-size: 12px;
resize: vertical;
box-sizing: border-box;
display: none;
`;
// Save button
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.style.cssText = `
margin: 5px 10px 10px 10px;
background: #007acc;
color: white;
border: none;
padding: 5px 15px;
cursor: pointer;
border-radius: 3px;
display: none;
`;
// Auto-save function
const autoSave = async () => {
await this.updateNoteContent(note.uuid, editContent.value);
note.content = editContent.value;
// Update view content
if (note.format_style === 'markdown') {
viewContent.innerHTML = this.renderMarkdown(note.content);
} else {
viewContent.textContent = note.content;
}
};
saveBtn.addEventListener('click', async () => {
await autoSave();
});
// Auto-save when clicking off the textarea
editContent.addEventListener('blur', async () => {
// Only auto-save if content has changed
if (editContent.value !== note.content) {
await autoSave();
}
});
contentContainer.appendChild(viewContent);
contentContainer.appendChild(editContent);
contentContainer.appendChild(saveBtn);
// Toggle between view and edit mode
if (modeToggle) {
const switchToView = async () => {
// Auto-save before switching to view
if (isEditMode && editContent.value !== note.content) {
await autoSave();
}
// Copy the current height from editContent to viewContent
const currentHeight = editContent.offsetHeight;
if (currentHeight > 0) {
viewContent.style.height = currentHeight + 'px';
}
isEditMode = false;
viewContent.style.display = 'block';
editContent.style.display = 'none';
saveBtn.style.display = 'none';
modeToggle.viewBtn.style.background = '#007acc';
modeToggle.editBtn.style.background = '#555';
};
const switchToEdit = () => {
isEditMode = true;
// Copy the current height from viewContent to editContent
const currentHeight = viewContent.offsetHeight;
if (currentHeight > 0) {
editContent.style.height = currentHeight + 'px';
}
viewContent.style.display = 'none';
editContent.style.display = 'block';
saveBtn.style.display = 'block';
modeToggle.viewBtn.style.background = '#555';
modeToggle.editBtn.style.background = '#007acc';
};
modeToggle.viewBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await switchToView();
});
modeToggle.editBtn.addEventListener('click', (e) => {
e.stopPropagation();
switchToEdit();
});
} else {
// For plain text, click to edit directly
viewContent.addEventListener('click', (e) => {
e.stopPropagation();
// Copy the current height from viewContent to editContent
const currentHeight = viewContent.offsetHeight;
if (currentHeight > 0) {
editContent.style.height = currentHeight + 'px';
}
viewContent.style.display = 'none';
editContent.style.display = 'block';
saveBtn.style.display = 'block';
editContent.focus();
});
}
// Collapsible functionality - restore previous state
const isExpanded = this.expandedNotes.has(note.uuid);
let isCollapsed = !isExpanded;
// Set initial state
contentContainer.style.display = isCollapsed ? 'none' : 'block';
collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
header.addEventListener('click', () => {
isCollapsed = !isCollapsed;
contentContainer.style.display = isCollapsed ? 'none' : 'block';
collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
// Update tracking state
if (isCollapsed) {
this.expandedNotes.delete(note.uuid);
} else {
this.expandedNotes.add(note.uuid);
}
});
// Context menu for sidebar notes
noteElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showSidebarNoteContextMenu(e, note);
});
noteElement.appendChild(header);
noteElement.appendChild(contentContainer);
this.notesContainer.appendChild(noteElement);
}
renderMarkdown(text) {
if (!text) return '';
// Use the bundled marked library if available
if (this.marked) {
try {
return this.marked.parse(text, {
breaks: true,
gfm: true
});
} catch (error) {
console.error('Markdown rendering error:', error);
}
}
// Fallback to plain text with line breaks
return text.replace(/\n/g, '<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 addNoteFromCurrentNode() {
// Check if a node is selected
if (!this.selectedNodeType) {
alert('Please select a node first');
return;
}
// Load folders first if not already loaded
await this.loadFolders();
// Show dialog to configure trigger and note details
const result = await this.showAddNoteFromCurrentNodeDialog();
if (!result) return;
try {
// Create the note
const response = await api.fetchApi('/autonotes/notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: result.name,
folder_uuid: result.folder_uuid
}),
});
const noteResult = await response.json();
if (noteResult.uuid) {
// Update the note with the trigger condition
await api.fetchApi(`/autonotes/notes/${noteResult.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
trigger_conditions: [result.triggerCondition]
}),
});
// Just refresh notes - don't open the edit dialog
await this.refreshNotes();
}
} catch (error) {
console.error('Failed to create note:', error);
alert('Failed to create note');
}
}
async showAddNoteDialog() {
return new Promise((resolve) => {
// Create overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 3000;
display: flex;
justify-content: center;
align-items: center;
`;
// Create dialog
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
padding: 20px;
width: 400px;
color: #fff;
font-family: Arial, sans-serif;
`;
// Title
const title = document.createElement('h2');
title.textContent = 'Create New Note';
title.style.cssText = `
margin: 0 0 20px 0;
font-size: 1.3em;
`;
// Name input
const nameLabel = document.createElement('label');
nameLabel.textContent = 'Note Name:';
nameLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.placeholder = 'Enter note name...';
nameInput.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 20px;
font-size: 14px;
`;
// Folder selection
const folderLabel = document.createElement('label');
folderLabel.textContent = 'Folder:';
folderLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const folderTreeContainer = document.createElement('div');
folderTreeContainer.style.cssText = `
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #1a1a1a;
border: 1px solid #555;
border-radius: 4px;
margin-bottom: 20px;
padding: 8px;
`;
let selectedFolderUuid = null;
// Root option
const rootItem = document.createElement('div');
rootItem.style.cssText = `
padding: 5px 8px;
cursor: pointer;
border-radius: 3px;
background: #007acc;
margin-bottom: 5px;
`;
rootItem.textContent = '📁 [Root]';
rootItem.dataset.folderUuid = '';
rootItem.addEventListener('click', () => {
// Remove selection from all items
folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => {
item.style.background = '';
});
rootItem.style.background = '#007acc';
selectedFolderUuid = null;
});
folderTreeContainer.appendChild(rootItem);
// Build folder tree recursively
// level 0 = [Root]
// level 1 = folders with parent_uuid = null (direct children of root)
// level 2+ = nested folders
const buildFolderTree = (parentUuid, level = 1) => {
// Filter folders by parent - handle both null and undefined
const childFolders = this.folders.filter(f => {
const folderParent = f.parent_uuid || null;
const searchParent = parentUuid || null;
return folderParent === searchParent;
});
for (const folder of childFolders) {
const folderItem = document.createElement('div');
const indent = 8 + (level * 20);
folderItem.style.cssText = `
padding: 5px 8px;
padding-left: ${indent}px;
cursor: pointer;
border-radius: 3px;
margin-bottom: 2px;
`;
folderItem.textContent = `${' '.repeat(level)}📁 ${folder.name}`;
folderItem.dataset.folderUuid = folder.uuid;
folderItem.addEventListener('click', () => {
// Remove selection from all items
folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => {
item.style.background = '';
});
folderItem.style.background = '#007acc';
selectedFolderUuid = folder.uuid;
});
folderTreeContainer.appendChild(folderItem);
// Recursively build child folders
buildFolderTree(folder.uuid, level + 1);
}
};
// Start building from root (null parent) at level 1
// Level 0 is [Root] itself, so children start at level 1
buildFolderTree(null, 1);
// Buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 20px;
cursor: pointer;
border-radius: 4px;
`;
const createBtn = document.createElement('button');
createBtn.textContent = 'Create';
createBtn.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 8px 20px;
cursor: pointer;
border-radius: 4px;
`;
// Event handlers
const closeDialog = (result) => {
overlay.remove();
resolve(result);
};
cancelBtn.addEventListener('click', () => closeDialog(null));
createBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a note name');
return;
}
closeDialog({
name: name,
folder_uuid: selectedFolderUuid
});
});
// Allow Enter key to create
nameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
createBtn.click();
}
});
// Focus name input
setTimeout(() => nameInput.focus(), 0);
// Assemble dialog
buttonContainer.appendChild(cancelBtn);
buttonContainer.appendChild(createBtn);
dialog.appendChild(title);
dialog.appendChild(nameLabel);
dialog.appendChild(nameInput);
dialog.appendChild(folderLabel);
dialog.appendChild(folderTreeContainer);
dialog.appendChild(buttonContainer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeDialog(null);
}
});
});
}
async openEditDialog(noteUuid = null) {
if (this.editDialog) {
this.editDialog.remove();
}
await this.loadFolders();
// Load all notes for the edit dialog, regardless of display mode
await this.loadAllNotes();
this.createEditDialog(noteUuid);
}
async loadAllNotes() {
try {
const response = await api.fetchApi('/autonotes/notes?mode=all');
this.notes = await response.json();
} catch (error) {
console.error('Failed to load all notes:', error);
}
}
async showAddNoteFromCurrentNodeDialog() {
return new Promise((resolve) => {
// Create overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 3000;
display: flex;
justify-content: center;
align-items: center;
`;
// Create dialog
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
padding: 20px;
width: 500px;
max-height: 80vh;
overflow-y: auto;
color: #fff;
font-family: Arial, sans-serif;
`;
// Title
const title = document.createElement('h2');
title.textContent = 'Create Note from Current Node';
title.style.cssText = `
margin: 0 0 20px 0;
font-size: 1.3em;
`;
// Current node info
const nodeInfo = document.createElement('div');
nodeInfo.style.cssText = `
background: #1a1a1a;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
border: 1px solid #555;
`;
const nodeInfoText = document.createElement('div');
nodeInfoText.textContent = `Selected Node: ${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}`;
nodeInfoText.style.fontWeight = 'bold';
nodeInfo.appendChild(nodeInfoText);
// Name input
const nameLabel = document.createElement('label');
nameLabel.textContent = 'Note Name:';
nameLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = `Note for ${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}`;
nameInput.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 20px;
font-size: 14px;
`;
// Trigger type selector
const triggerTypeLabel = document.createElement('label');
triggerTypeLabel.textContent = 'Trigger Type:';
triggerTypeLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const triggerTypeSelect = document.createElement('select');
triggerTypeSelect.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 20px;
font-size: 14px;
`;
triggerTypeSelect.innerHTML = `
<option value="node_selected">Node Selected - Shows when this node type is selected</option>
<option value="node_selected_attribute">Node Selected + Attribute Value - Shows when this node has specific attribute value</option>
<option value="node_in_workflow">Node In Workflow - Shows when this node exists in workflow</option>
<option value="node_in_workflow_attribute">Node In Workflow + Attribute Value - Shows when this node has an attribute in workflow</option>
`;
// Dynamic fields container
const dynamicFieldsContainer = document.createElement('div');
dynamicFieldsContainer.style.marginBottom = '20px';
// Trigger condition object
const triggerCondition = {
type: 'node_selected',
node_types: [this.selectedNodeType],
node_type: this.selectedNodeType,
attribute_name: null,
attribute_values: [],
workflow_names: []
};
// Function to update dynamic fields based on trigger type
const updateDynamicFields = () => {
dynamicFieldsContainer.innerHTML = '';
triggerCondition.type = triggerTypeSelect.value;
if (triggerCondition.type === 'node_selected') {
// Already set with current node type
triggerCondition.node_types = [this.selectedNodeType];
const info = document.createElement('div');
info.style.cssText = `
background: #1a1a1a;
padding: 10px;
border-radius: 4px;
border: 1px solid #555;
color: #aaa;
`;
info.textContent = `This note will appear when "${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}" is selected.`;
dynamicFieldsContainer.appendChild(info);
} else if (triggerCondition.type === 'node_selected_attribute') {
// Attribute selector
const attrLabel = document.createElement('label');
attrLabel.textContent = 'Attribute:';
attrLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const attrSelect = document.createElement('select');
attrSelect.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
margin-bottom: 10px;
font-size: 14px;
`;
// Populate with node attributes
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = '-- Select Attribute --';
attrSelect.appendChild(emptyOption);
if (this.nodeTypes[this.selectedNodeType]) {
const nodeInfo = this.nodeTypes[this.selectedNodeType];
const inputs = nodeInfo.input?.required || {};
const optionalInputs = nodeInfo.input?.optional || {};
const allInputs = { ...inputs, ...optionalInputs };
Object.keys(allInputs).forEach(attrName => {
const option = document.createElement('option');
option.value = attrName;
option.textContent = attrName;
attrSelect.appendChild(option);
});
}
// Value input
const valueLabel = document.createElement('label');
valueLabel.textContent = 'Attribute Value (to match):';
valueLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Enter value...';
valueInput.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
font-size: 14px;
`;
// Update value input when attribute changes
attrSelect.addEventListener('change', () => {
triggerCondition.attribute_name = attrSelect.value;
// Pre-fill with current value if available
if (this.selectedNodeAttributes[attrSelect.value] !== undefined) {
valueInput.value = String(this.selectedNodeAttributes[attrSelect.value]);
triggerCondition.attribute_values = [valueInput.value];
} else {
valueInput.value = '';
triggerCondition.attribute_values = [];
}
});
valueInput.addEventListener('input', () => {
triggerCondition.attribute_values = valueInput.value ? [valueInput.value] : [];
});
dynamicFieldsContainer.appendChild(attrLabel);
dynamicFieldsContainer.appendChild(attrSelect);
dynamicFieldsContainer.appendChild(valueLabel);
dynamicFieldsContainer.appendChild(valueInput);
} else if (triggerCondition.type === 'node_in_workflow') {
// Already set with current node type
triggerCondition.node_types = [this.selectedNodeType];
const info = document.createElement('div');
info.style.cssText = `
background: #1a1a1a;
padding: 10px;
border-radius: 4px;
border: 1px solid #555;
color: #aaa;
`;
info.textContent = `This note will appear when "${this.nodeTypes[this.selectedNodeType]?.display_name || this.selectedNodeType}" exists anywhere in the workflow.`;
dynamicFieldsContainer.appendChild(info);
} else if (triggerCondition.type === 'node_in_workflow_attribute') {
// Attribute selector
const attrLabel = document.createElement('label');
attrLabel.textContent = 'Attribute:';
attrLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const attrSelect = document.createElement('select');
attrSelect.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
margin-bottom: 10px;
font-size: 14px;
`;
// Populate with node attributes
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = '-- Select Attribute --';
attrSelect.appendChild(emptyOption);
if (this.nodeTypes[this.selectedNodeType]) {
const nodeInfo = this.nodeTypes[this.selectedNodeType];
const inputs = nodeInfo.input?.required || {};
const optionalInputs = nodeInfo.input?.optional || {};
const allInputs = { ...inputs, ...optionalInputs };
Object.keys(allInputs).forEach(attrName => {
const option = document.createElement('option');
option.value = attrName;
option.textContent = attrName;
attrSelect.appendChild(option);
});
}
// Value input (optional)
const valueLabel = document.createElement('label');
valueLabel.textContent = 'Attribute Value (optional, leave blank for any):';
valueLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Enter value or leave blank...';
valueInput.style.cssText = `
width: 100%;
padding: 8px;
background: #1a1a1a;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
font-size: 14px;
`;
// Update value input when attribute changes
attrSelect.addEventListener('change', () => {
triggerCondition.attribute_name = attrSelect.value;
// Pre-fill with current value if available
if (this.selectedNodeAttributes[attrSelect.value] !== undefined) {
valueInput.value = String(this.selectedNodeAttributes[attrSelect.value]);
triggerCondition.attribute_values = [valueInput.value];
} else {
valueInput.value = '';
triggerCondition.attribute_values = [];
}
});
valueInput.addEventListener('input', () => {
triggerCondition.attribute_values = valueInput.value ? [valueInput.value] : [];
});
dynamicFieldsContainer.appendChild(attrLabel);
dynamicFieldsContainer.appendChild(attrSelect);
dynamicFieldsContainer.appendChild(valueLabel);
dynamicFieldsContainer.appendChild(valueInput);
}
};
triggerTypeSelect.addEventListener('change', updateDynamicFields);
updateDynamicFields();
// Folder selection
const folderLabel = document.createElement('label');
folderLabel.textContent = 'Folder:';
folderLabel.style.cssText = `
display: block;
margin-bottom: 5px;
`;
const folderTreeContainer = document.createElement('div');
folderTreeContainer.style.cssText = `
max-height: 150px;
overflow-y: auto;
background: #1a1a1a;
border: 1px solid #555;
border-radius: 4px;
padding: 10px;
margin-bottom: 20px;
`;
let selectedFolderUuid = null;
// Build folder tree
const buildFolderTree = (parentUuid, level) => {
const children = this.folders.filter(f => f.parent_uuid === parentUuid);
children.forEach(folder => {
const folderOption = document.createElement('div');
folderOption.style.cssText = `
padding: 5px;
padding-left: ${level * 15}px;
cursor: pointer;
border-radius: 3px;
`;
folderOption.textContent = `📁 ${folder.name}`;
folderOption.addEventListener('mouseenter', () => {
folderOption.style.background = '#007acc';
});
folderOption.addEventListener('mouseleave', () => {
if (selectedFolderUuid !== folder.uuid) {
folderOption.style.background = '';
}
});
folderOption.addEventListener('click', () => {
// Deselect previous
folderTreeContainer.querySelectorAll('div').forEach(el => {
el.style.background = '';
});
selectedFolderUuid = folder.uuid;
folderOption.style.background = '#007acc';
});
folderTreeContainer.appendChild(folderOption);
buildFolderTree(folder.uuid, level + 1);
});
};
// Add root option
const rootOption = document.createElement('div');
rootOption.style.cssText = `
padding: 5px;
cursor: pointer;
border-radius: 3px;
background: #007acc;
`;
rootOption.textContent = '[Root]';
rootOption.addEventListener('mouseenter', () => {
rootOption.style.background = '#007acc';
});
rootOption.addEventListener('mouseleave', () => {
if (selectedFolderUuid !== null) {
rootOption.style.background = '';
}
});
rootOption.addEventListener('click', () => {
folderTreeContainer.querySelectorAll('div').forEach(el => {
el.style.background = '';
});
selectedFolderUuid = null;
rootOption.style.background = '#007acc';
});
folderTreeContainer.appendChild(rootOption);
buildFolderTree(null, 1);
// Buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
cancelBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(null);
});
const createBtn = document.createElement('button');
createBtn.textContent = 'Create';
createBtn.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
createBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a note name');
return;
}
// Validate trigger condition
if (triggerCondition.type === 'node_selected_attribute' || triggerCondition.type === 'node_in_workflow_attribute') {
if (!triggerCondition.attribute_name) {
alert('Please select an attribute');
return;
}
}
document.body.removeChild(overlay);
resolve({
name,
folder_uuid: selectedFolderUuid,
triggerCondition
});
});
// Handle Enter key
nameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
createBtn.click();
}
});
// Focus name input
setTimeout(() => nameInput.focus(), 0);
// Assemble dialog
buttonContainer.appendChild(cancelBtn);
buttonContainer.appendChild(createBtn);
dialog.appendChild(title);
dialog.appendChild(nodeInfo);
dialog.appendChild(nameLabel);
dialog.appendChild(nameInput);
dialog.appendChild(triggerTypeLabel);
dialog.appendChild(triggerTypeSelect);
dialog.appendChild(dynamicFieldsContainer);
dialog.appendChild(folderLabel);
dialog.appendChild(folderTreeContainer);
dialog.appendChild(buttonContainer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
resolve(null);
}
});
});
}
async loadFolders() {
try {
const response = await api.fetchApi('/autonotes/folders');
this.folders = await response.json();
} catch (error) {
console.error('Failed to load folders:', error);
this.folders = [];
}
}
createEditDialog(selectedNoteUuid = null) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
`;
// Load saved dialog size
const savedWidth = localStorage.getItem('autonotes_editDialogWidth') || '800px';
const savedHeight = localStorage.getItem('autonotes_editDialogHeight') || '600px';
// Create dialog
this.editDialog = document.createElement('div');
this.editDialog.style.cssText = `
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
width: ${savedWidth};
height: ${savedHeight};
display: flex;
color: #fff;
font-family: Arial, sans-serif;
position: relative;
`;
// Track if we're currently resizing to prevent closing on overlay click
this.isResizingDialog = false;
// Create resize handles
const createResizeHandle = (position, cursor) => {
const handle = document.createElement('div');
handle.style.cssText = `
position: absolute;
background: transparent;
z-index: 10;
`;
if (position === 'right') {
handle.style.cssText += `
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: ${cursor};
`;
} else if (position === 'bottom') {
handle.style.cssText += `
bottom: 0;
left: 0;
width: 100%;
height: 5px;
cursor: ${cursor};
`;
} else if (position === 'corner') {
handle.style.cssText += `
right: 0;
bottom: 0;
width: 15px;
height: 15px;
cursor: ${cursor};
`;
}
let isResizing = false;
let startX, startY, startWidth, startHeight;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
this.isResizingDialog = true;
startX = e.clientX;
startY = e.clientY;
startWidth = this.editDialog.offsetWidth;
startHeight = this.editDialog.offsetHeight;
document.body.style.userSelect = 'none';
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
if (position === 'right' || position === 'corner') {
const newWidth = startWidth + (e.clientX - startX);
this.editDialog.style.width = Math.max(400, Math.min(window.innerWidth - 40, newWidth)) + 'px';
}
if (position === 'bottom' || position === 'corner') {
const newHeight = startHeight + (e.clientY - startY);
this.editDialog.style.height = Math.max(300, Math.min(window.innerHeight - 40, newHeight)) + 'px';
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.userSelect = '';
// Save size to localStorage
localStorage.setItem('autonotes_editDialogWidth', this.editDialog.style.width);
localStorage.setItem('autonotes_editDialogHeight', this.editDialog.style.height);
// Delay clearing the flag to prevent overlay click from closing
setTimeout(() => {
this.isResizingDialog = false;
}, 100);
}
});
return handle;
};
this.editDialog.appendChild(createResizeHandle('right', 'ew-resize'));
this.editDialog.appendChild(createResizeHandle('bottom', 'ns-resize'));
this.editDialog.appendChild(createResizeHandle('corner', 'nwse-resize'));
// Tree view (left side)
const treeContainer = document.createElement('div');
treeContainer.style.cssText = `
width: 250px;
border-right: 1px solid #555;
padding: 10px;
overflow-y: auto;
`;
const treeHeader = document.createElement('div');
treeHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
`;
const treeTitle = document.createElement('h3');
treeTitle.textContent = 'Notes';
treeTitle.style.margin = '0';
const folderButtons = document.createElement('div');
folderButtons.style.display = 'flex';
folderButtons.style.gap = '5px';
const addFolderButton = document.createElement('button');
addFolderButton.textContent = '+';
addFolderButton.title = 'Add Folder';
addFolderButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 2px 8px;
cursor: pointer;
border-radius: 3px;
font-size: 16px;
`;
addFolderButton.addEventListener('click', () => this.addFolder(treeContainer, selectedNoteUuid));
folderButtons.appendChild(addFolderButton);
treeHeader.appendChild(treeTitle);
treeHeader.appendChild(folderButtons);
treeContainer.appendChild(treeHeader);
this.renderNoteTree(treeContainer, selectedNoteUuid);
// Make tree container a drop target for root
treeContainer.addEventListener('dragover', (e) => {
// Check if we're over empty space (not over a folder or note)
const target = e.target;
if (target === treeContainer || target === treeHeader || target === treeTitle) {
e.preventDefault();
treeContainer.style.background = 'rgba(0, 122, 204, 0.1)';
}
});
treeContainer.addEventListener('dragleave', (e) => {
if (e.target === treeContainer) {
treeContainer.style.background = '';
}
});
treeContainer.addEventListener('drop', async (e) => {
// Only handle if dropped on empty space
const target = e.target;
if (target === treeContainer || target === treeHeader || target === treeTitle) {
e.preventDefault();
treeContainer.style.background = '';
const noteUuid = e.dataTransfer.getData('application/x-note-uuid');
const folderUuid = e.dataTransfer.getData('application/x-folder-uuid');
if (noteUuid) {
await this.moveNoteToFolder(noteUuid, null, treeContainer, selectedNoteUuid);
} else if (folderUuid) {
await this.moveFolderToFolder(folderUuid, null, treeContainer, selectedNoteUuid);
}
}
});
// Edit panel (right side)
const editPanel = document.createElement('div');
editPanel.style.cssText = `
flex: 1;
padding: 10px;
display: flex;
flex-direction: column;
`;
this.createEditPanel(editPanel, selectedNoteUuid);
this.editDialog.appendChild(treeContainer);
this.editDialog.appendChild(editPanel);
// Auto-save when clicking anywhere in the dialog (except textarea)
this.editDialog.addEventListener('click', async (e) => {
if (e.target !== this.contentTextarea && this.currentEditingNote && this.textareaChanged) {
await this.saveCurrentNote();
this.textareaChanged = false;
}
});
// Auto-save when clicking on the overlay (outside dialog)
overlay.addEventListener('click', async (e) => {
if (e.target === overlay && this.currentEditingNote && this.textareaChanged) {
await this.saveCurrentNote();
this.textareaChanged = false;
}
});
// Global document-level click handler for auto-save (catches clicks outside dialog)
this.globalAutoSaveHandler = async (e) => {
// Only auto-save if dialog is open and there are changes
if (this.editDialog && this.currentEditingNote && this.textareaChanged) {
// Check if click is outside the dialog and not on the textarea
if (!this.editDialog.contains(e.target) && e.target !== this.contentTextarea) {
await this.saveCurrentNote();
this.textareaChanged = false;
}
}
};
// Add the handler with a slight delay to avoid immediate triggering
setTimeout(() => {
document.addEventListener('click', this.globalAutoSaveHandler, true);
}, 100);
overlay.appendChild(this.editDialog);
document.body.appendChild(overlay);
}
renderNoteTree(container, selectedNoteUuid) {
// Clear existing tree items (but not the header)
const existingItems = container.querySelectorAll('.tree-item, .folder-item');
existingItems.forEach(item => item.remove());
// Render only root-level folders (parent_uuid === null)
const rootFolders = this.folders.filter(f => !f.parent_uuid);
for (const folder of rootFolders) {
const folderItem = this.createFolderItem(folder, container, selectedNoteUuid);
container.appendChild(folderItem);
}
// Render notes (only top-level notes without folders)
for (const note of this.notes) {
if (!note.folder_uuid) {
const noteItem = this.createNoteItem(note, selectedNoteUuid, container);
container.appendChild(noteItem);
}
}
}
createFolderItem(folder, treeContainer, selectedNoteUuid) {
const folderItem = document.createElement('div');
folderItem.className = 'folder-item';
folderItem.style.cssText = `
margin-bottom: 5px;
`;
const folderHeader = document.createElement('div');
folderHeader.draggable = true;
folderHeader.style.cssText = `
display: flex;
align-items: center;
gap: 5px;
padding: 5px;
border-radius: 3px;
background: #333;
cursor: pointer;
`;
// Collapse/expand icon
const expandIcon = document.createElement('span');
expandIcon.textContent = '▶';
expandIcon.style.cssText = `
transition: transform 0.2s;
font-size: 10px;
`;
// Make folder draggable
folderHeader.addEventListener('dragstart', (e) => {
e.stopPropagation();
e.dataTransfer.setData('application/x-folder-uuid', folder.uuid);
e.dataTransfer.effectAllowed = 'move';
folderHeader.style.opacity = '0.5';
});
folderHeader.addEventListener('dragend', (e) => {
folderHeader.style.opacity = '1';
});
// Make folder a drop target
folderHeader.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
folderHeader.style.background = '#007acc';
});
folderHeader.addEventListener('dragleave', (e) => {
e.stopPropagation();
folderHeader.style.background = '#333';
});
folderHeader.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
folderHeader.style.background = '#333';
// Check if it's a note or folder being dropped
const noteUuid = e.dataTransfer.getData('application/x-note-uuid');
const folderUuid = e.dataTransfer.getData('application/x-folder-uuid');
if (noteUuid) {
await this.moveNoteToFolder(noteUuid, folder.uuid, treeContainer, selectedNoteUuid);
} else if (folderUuid && folderUuid !== folder.uuid) {
// Prevent dropping a folder into itself
await this.moveFolderToFolder(folderUuid, folder.uuid, treeContainer, selectedNoteUuid);
}
});
// Right-click context menu
folderHeader.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
this.showFolderContextMenu(e, folder, treeContainer, selectedNoteUuid);
});
const folderName = document.createElement('span');
folderName.textContent = `📁 ${folder.name}`;
folderName.style.flex = '1';
const renameBtn = document.createElement('button');
renameBtn.textContent = '✏️';
renameBtn.title = 'Rename Folder';
renameBtn.style.cssText = `
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 2px;
font-size: 12px;
`;
renameBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.renameFolder(folder, treeContainer, selectedNoteUuid);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.title = 'Delete Folder';
deleteBtn.style.cssText = `
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 2px;
font-size: 12px;
`;
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deleteFolder(folder, treeContainer, selectedNoteUuid);
});
folderHeader.appendChild(expandIcon);
folderHeader.appendChild(folderName);
folderHeader.appendChild(renameBtn);
folderHeader.appendChild(deleteBtn);
// Children container (child folders and notes)
const childrenContainer = document.createElement('div');
childrenContainer.style.cssText = `
padding-left: 20px;
display: none;
`;
// Find child folders
const childFolders = this.folders.filter(f => f.parent_uuid === folder.uuid);
for (const childFolder of childFolders) {
const childFolderItem = this.createFolderItem(childFolder, treeContainer, selectedNoteUuid);
childrenContainer.appendChild(childFolderItem);
}
// Find notes in this folder
const folderNotes = this.notes.filter(note => note.folder_uuid === folder.uuid);
for (const note of folderNotes) {
const noteItem = this.createNoteItem(note, selectedNoteUuid, treeContainer);
childrenContainer.appendChild(noteItem);
}
// Toggle expand/collapse
const isExpanded = this.expandedFolders.has(folder.uuid);
childrenContainer.style.display = isExpanded ? 'block' : 'none';
expandIcon.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)';
folderHeader.addEventListener('click', (e) => {
// Don't toggle if clicking buttons
if (e.target === renameBtn || e.target === deleteBtn) {
return;
}
if (this.expandedFolders.has(folder.uuid)) {
this.expandedFolders.delete(folder.uuid);
childrenContainer.style.display = 'none';
expandIcon.style.transform = 'rotate(0deg)';
} else {
this.expandedFolders.add(folder.uuid);
childrenContainer.style.display = 'block';
expandIcon.style.transform = 'rotate(90deg)';
}
});
folderItem.appendChild(folderHeader);
folderItem.appendChild(childrenContainer);
return folderItem;
}
createNoteItem(note, selectedNoteUuid, treeContainer) {
const noteItem = document.createElement('div');
noteItem.className = 'tree-item';
noteItem.draggable = true;
noteItem.style.cssText = `
padding: 5px;
cursor: move;
border-radius: 3px;
margin-bottom: 2px;
${note.uuid === selectedNoteUuid ? 'background: #007acc;' : ''}
`;
noteItem.textContent = note.name || 'Untitled';
// Drag start
noteItem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/x-note-uuid', note.uuid);
e.dataTransfer.effectAllowed = 'move';
noteItem.style.opacity = '0.5';
});
// Drag end
noteItem.addEventListener('dragend', (e) => {
noteItem.style.opacity = '1';
});
// Left click to select
noteItem.addEventListener('click', async () => {
// Auto-save current note before switching
if (this.currentEditingNote && this.textareaChanged) {
await this.saveCurrentNote();
this.textareaChanged = false;
}
// Remove previous selection
treeContainer.querySelectorAll('.tree-item').forEach(item => {
item.style.background = '';
});
noteItem.style.background = '#007acc';
this.loadNoteIntoEditor(note.uuid);
});
// Right click for context menu
noteItem.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showNoteContextMenu(e, note, treeContainer, selectedNoteUuid);
});
return noteItem;
}
createEditPanel(panel, selectedNoteUuid) {
// Content textarea
const contentContainer = document.createElement('div');
contentContainer.style.cssText = `
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 10px;
`;
const contentLabel = document.createElement('label');
contentLabel.textContent = 'Content: ';
contentLabel.style.display = 'block';
contentLabel.style.marginBottom = '5px';
this.contentTextarea = document.createElement('textarea');
this.contentTextarea.style.cssText = `
flex: 1;
padding: 10px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
resize: none;
font-family: monospace;
`;
// Track if content has changed
this.textareaChanged = false;
// Auto-save on input (debounced)
let saveTimeout;
this.contentTextarea.addEventListener('input', () => {
this.textareaChanged = true;
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
if (this.currentEditingNote && this.textareaChanged) {
await this.saveCurrentNote();
this.textareaChanged = false;
}
}, 1000); // Auto-save after 1 second of inactivity
});
// Auto-save on blur (when clicking outside the textarea)
this.contentTextarea.addEventListener('blur', async () => {
if (this.currentEditingNote && this.textareaChanged) {
clearTimeout(saveTimeout);
await this.saveCurrentNote();
this.textareaChanged = false;
}
});
contentContainer.appendChild(contentLabel);
contentContainer.appendChild(this.contentTextarea);
// Format style
const formatContainer = document.createElement('div');
formatContainer.style.cssText = `
display: flex;
gap: 20px;
margin-bottom: 10px;
align-items: center;
`;
const formatDiv = document.createElement('div');
const formatLabel = document.createElement('label');
formatLabel.textContent = 'Format: ';
formatLabel.style.marginRight = '10px';
this.formatSelect = document.createElement('select');
this.formatSelect.style.cssText = `
background: #333;
color: #fff;
border: 1px solid #555;
padding: 5px;
`;
this.formatSelect.innerHTML = `
<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);
// Tags section
const tagsContainer = document.createElement('div');
tagsContainer.style.cssText = `
margin-bottom: 10px;
border: 1px solid #555;
border-radius: 4px;
padding: 10px;
`;
const tagsHeader = document.createElement('div');
tagsHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
`;
const tagsTitle = document.createElement('h4');
tagsTitle.textContent = 'Tags';
tagsTitle.style.margin = '0';
const addTagButton = document.createElement('button');
addTagButton.textContent = '+ Add Tag';
addTagButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
font-size: 11px;
`;
tagsHeader.appendChild(tagsTitle);
tagsHeader.appendChild(addTagButton);
this.tagsListContainer = document.createElement('div');
this.tagsListContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 5px;
`;
this.currentTags = [];
addTagButton.addEventListener('click', () => {
const tag = prompt('Enter tag name:');
if (tag && tag.trim()) {
const trimmedTag = tag.trim();
if (!this.currentTags.includes(trimmedTag)) {
this.currentTags.push(trimmedTag);
this.renderTags();
this.textareaChanged = true; // Mark as changed for auto-save
}
}
});
tagsContainer.appendChild(tagsHeader);
tagsContainer.appendChild(this.tagsListContainer);
// Trigger conditions section
const triggerContainer = document.createElement('div');
triggerContainer.style.cssText = `
margin-bottom: 10px;
border: 1px solid #555;
border-radius: 4px;
padding: 10px;
max-height: 200px;
overflow-y: auto;
`;
const triggerTitle = document.createElement('h4');
triggerTitle.textContent = 'Trigger Conditions';
triggerTitle.style.margin = '0 0 10px 0';
const addTriggerButton = document.createElement('button');
addTriggerButton.textContent = 'Add Trigger';
addTriggerButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
margin-bottom: 10px;
`;
addTriggerButton.addEventListener('click', () => this.addTriggerCondition());
this.triggerList = document.createElement('div');
triggerContainer.appendChild(triggerTitle);
triggerContainer.appendChild(addTriggerButton);
triggerContainer.appendChild(this.triggerList);
// Buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: space-between;
margin-top: 10px;
`;
const leftButtons = document.createElement('div');
leftButtons.style.display = 'flex';
leftButtons.style.gap = '10px';
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
saveButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
saveButton.addEventListener('click', () => this.saveCurrentNote());
const revertButton = document.createElement('button');
revertButton.textContent = 'Revert';
revertButton.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
leftButtons.appendChild(saveButton);
leftButtons.appendChild(revertButton);
const rightButtons = document.createElement('div');
rightButtons.style.display = 'flex';
rightButtons.style.gap = '10px';
const okButton = document.createElement('button');
okButton.textContent = 'OK';
okButton.style.cssText = `
background: #007acc;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
okButton.addEventListener('click', () => this.closeEditDialog(true));
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
`;
cancelButton.addEventListener('click', () => this.closeEditDialog(false));
rightButtons.appendChild(okButton);
rightButtons.appendChild(cancelButton);
buttonContainer.appendChild(leftButtons);
buttonContainer.appendChild(rightButtons);
panel.appendChild(contentContainer);
panel.appendChild(formatContainer);
panel.appendChild(tagsContainer);
panel.appendChild(triggerContainer);
panel.appendChild(buttonContainer);
// Load initial note if specified
if (selectedNoteUuid) {
this.loadNoteIntoEditor(selectedNoteUuid);
}
}
loadNoteIntoEditor(noteUuid) {
const note = this.notes.find(n => n.uuid === noteUuid);
if (!note) return;
this.currentEditingNote = note;
this.contentTextarea.value = note.content || '';
this.formatSelect.value = note.format_style || 'plaintext';
this.pinnedCheckbox.checked = note.pinned || false;
// Load tags
this.currentTags = [...(note.tags || [])];
this.renderTags();
// Load trigger conditions
this.currentTriggerConditions = [...(note.trigger_conditions || [])];
this.renderTriggerConditions();
}
renderTags() {
this.tagsListContainer.innerHTML = '';
for (const tag of this.currentTags) {
const tagElement = document.createElement('div');
tagElement.style.cssText = `
background: #007acc;
color: white;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
`;
const tagText = document.createElement('span');
tagText.textContent = tag;
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.cssText = `
background: transparent;
border: none;
color: white;
cursor: pointer;
font-size: 16px;
padding: 0;
line-height: 1;
`;
removeBtn.addEventListener('click', () => {
this.currentTags = this.currentTags.filter(t => t !== tag);
this.renderTags();
this.textareaChanged = true; // Mark as changed for auto-save
});
tagElement.appendChild(tagText);
tagElement.appendChild(removeBtn);
this.tagsListContainer.appendChild(tagElement);
}
}
addTriggerCondition() {
const condition = {
type: 'node_selected',
node_types: [],
node_type: null,
attribute_name: null,
attribute_values: [],
workflow_names: []
};
this.currentTriggerConditions = this.currentTriggerConditions || [];
this.currentTriggerConditions.push(condition);
this.renderTriggerConditions();
}
renderTriggerConditions() {
this.triggerList.innerHTML = '';
if (!this.currentTriggerConditions) {
this.currentTriggerConditions = [];
}
this.currentTriggerConditions.forEach((condition, index) => {
const conditionElement = this.createTriggerConditionElement(condition, index);
this.triggerList.appendChild(conditionElement);
});
}
createTriggerConditionElement(condition, index) {
const container = document.createElement('div');
container.style.cssText = `
border: 1px solid #666;
border-radius: 3px;
margin-bottom: 10px;
background: #333;
`;
// Header (collapsible)
const header = document.createElement('div');
header.style.cssText = `
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
background: #3a3a3a;
border-radius: 3px 3px 0 0;
`;
// Collapse icon
const collapseIcon = document.createElement('span');
collapseIcon.textContent = '▼';
collapseIcon.style.cssText = `
transition: transform 0.2s;
margin-right: 10px;
font-size: 10px;
`;
// Title showing condition type
const titleSpan = document.createElement('span');
titleSpan.style.flex = '1';
const getConditionTitle = () => {
if (condition.type === 'node_selected') return 'Node Selected';
if (condition.type === 'node_selected_attribute') return 'Node Selected + Attribute Value';
if (condition.type === 'node_in_workflow') return 'Node In Workflow';
if (condition.type === 'node_in_workflow_attribute') return 'Node In Workflow + Attribute Value';
if (condition.type === 'workflow_name') return 'Workflow Name';
return 'Trigger Condition';
};
titleSpan.textContent = getConditionTitle();
header.appendChild(collapseIcon);
header.appendChild(titleSpan);
// Content container (collapsible)
const content = document.createElement('div');
content.style.cssText = `
padding: 10px;
display: block;
`;
// Type selector
const typeContainer = document.createElement('div');
typeContainer.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 10px;
`;
const typeLabel = document.createElement('label');
typeLabel.textContent = 'Type: ';
typeLabel.style.marginRight = '10px';
const typeSelect = document.createElement('select');
typeSelect.style.cssText = `
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
padding: 5px;
margin-right: 10px;
`;
typeSelect.innerHTML = `
<option value="node_selected">Node Selected</option>
<option value="node_selected_attribute">Node Selected + Attribute Value</option>
<option value="node_in_workflow">Node In Workflow</option>
<option value="node_in_workflow_attribute">Node In Workflow + Attribute Value</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_selected_attribute') {
this.createNodeAttributeFields(fieldsContainer, condition);
} else if (condition.type === 'node_in_workflow') {
this.createNodeInWorkflowFields(fieldsContainer, condition);
} else if (condition.type === 'node_in_workflow_attribute') {
this.createAttributeInWorkflowFields(fieldsContainer, condition);
} else if (condition.type === 'workflow_name') {
this.createWorkflowNameFields(fieldsContainer, condition);
}
};
typeSelect.addEventListener('change', updateContent);
updateContent();
content.appendChild(typeContainer);
content.appendChild(fieldsContainer);
// Collapse/expand functionality
let isCollapsed = false;
header.addEventListener('click', (e) => {
// Don't collapse if clicking on buttons/inputs
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') {
return;
}
isCollapsed = !isCollapsed;
content.style.display = isCollapsed ? 'none' : 'block';
collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
});
container.appendChild(header);
container.appendChild(content);
return container;
}
createNodeSelectedFields(container, condition) {
const label = document.createElement('label');
label.textContent = 'Node Types: ';
label.style.display = 'block';
label.style.marginBottom = '5px';
// Multi-select dropdown with searchable autocomplete
const selectContainer = document.createElement('div');
selectContainer.style.cssText = `
position: relative;
width: 100%;
`;
// Create a searchable input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search node types...';
searchInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
margin-bottom: 5px;
`;
// Selected items display
const selectedDisplay = document.createElement('div');
selectedDisplay.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
min-height: 30px;
padding: 5px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
`;
// Dropdown list
const dropdownList = document.createElement('div');
dropdownList.style.cssText = `
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
z-index: 1000;
display: none;
`;
// Get node types from workflow
const workflowNodeTypes = new Set();
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
workflowNodeTypes.add(node.type);
}
}
// Populate dropdown with all node types
const nodeTypesList = Object.keys(this.nodeTypes).sort();
const updateSelectedDisplay = () => {
selectedDisplay.innerHTML = '';
(condition.node_types || []).forEach(nodeType => {
const tag = document.createElement('span');
tag.style.cssText = `
background: #007acc;
color: #fff;
padding: 3px 8px;
border-radius: 3px;
display: inline-flex;
align-items: center;
gap: 5px;
`;
// Add icon if in workflow
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
tag.appendChild(icon);
}
const nameSpan = document.createElement('span');
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
tag.appendChild(nameSpan);
const removeBtn = document.createElement('span');
removeBtn.textContent = '×';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontWeight = 'bold';
removeBtn.addEventListener('click', () => {
condition.node_types = condition.node_types.filter(t => t !== nodeType);
updateSelectedDisplay();
});
tag.appendChild(removeBtn);
selectedDisplay.appendChild(tag);
});
};
const filterDropdown = (searchTerm) => {
dropdownList.innerHTML = '';
const filtered = nodeTypesList.filter(nodeType => {
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
return displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
nodeType.toLowerCase().includes(searchTerm.toLowerCase());
});
filtered.forEach(nodeType => {
const option = document.createElement('div');
option.style.cssText = `
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
`;
// Add checkmark if in workflow
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
icon.style.fontWeight = 'bold';
option.appendChild(icon);
}
const nameSpan = document.createElement('span');
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
option.appendChild(nameSpan);
option.addEventListener('mouseenter', () => {
option.style.background = '#007acc';
});
option.addEventListener('mouseleave', () => {
option.style.background = '';
});
option.addEventListener('click', () => {
if (!condition.node_types) condition.node_types = [];
if (!condition.node_types.includes(nodeType)) {
condition.node_types.push(nodeType);
updateSelectedDisplay();
}
searchInput.value = '';
dropdownList.style.display = 'none';
});
dropdownList.appendChild(option);
});
dropdownList.style.display = filtered.length > 0 ? 'block' : 'none';
};
searchInput.addEventListener('focus', () => {
filterDropdown(searchInput.value);
});
searchInput.addEventListener('input', () => {
filterDropdown(searchInput.value);
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!selectContainer.contains(e.target)) {
dropdownList.style.display = 'none';
}
});
selectContainer.appendChild(searchInput);
selectContainer.appendChild(dropdownList);
container.appendChild(label);
container.appendChild(selectedDisplay);
container.appendChild(selectContainer);
// Initialize display
updateSelectedDisplay();
}
createNodeAttributeFields(container, condition) {
// Node type searchable dropdown
const nodeTypeContainer = document.createElement('div');
nodeTypeContainer.style.marginBottom = '10px';
const nodeTypeLabel = document.createElement('label');
nodeTypeLabel.textContent = 'Node Type: ';
nodeTypeLabel.style.display = 'block';
nodeTypeLabel.style.marginBottom = '5px';
// Get node types from workflow
const workflowNodeTypes = new Set();
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
workflowNodeTypes.add(node.type);
}
}
const selectContainer = document.createElement('div');
selectContainer.style.cssText = `
position: relative;
width: 100%;
`;
// Display selected value
const selectedDisplay = document.createElement('div');
selectedDisplay.style.cssText = `
width: 100%;
padding: 8px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
`;
const updateSelectedDisplay = () => {
selectedDisplay.innerHTML = '';
if (condition.node_type) {
if (workflowNodeTypes.has(condition.node_type)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
icon.style.fontWeight = 'bold';
selectedDisplay.appendChild(icon);
}
const text = document.createElement('span');
text.textContent = this.nodeTypes[condition.node_type]?.display_name || condition.node_type;
selectedDisplay.appendChild(text);
} else {
const text = document.createElement('span');
text.textContent = '-- Select Node Type --';
text.style.color = '#888';
selectedDisplay.appendChild(text);
}
};
// Search input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search node types...';
searchInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
display: none;
`;
// Dropdown list
const dropdownList = document.createElement('div');
dropdownList.style.cssText = `
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
z-index: 1000;
display: none;
margin-top: 2px;
`;
const nodeTypesList = Object.keys(this.nodeTypes).sort();
const filterDropdown = (searchTerm) => {
dropdownList.innerHTML = '';
const filtered = nodeTypesList.filter(nodeType => {
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
return displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
nodeType.toLowerCase().includes(searchTerm.toLowerCase());
});
filtered.forEach(nodeType => {
const option = document.createElement('div');
option.style.cssText = `
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
`;
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
icon.style.fontWeight = 'bold';
option.appendChild(icon);
}
const nameSpan = document.createElement('span');
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
option.appendChild(nameSpan);
option.addEventListener('mouseenter', () => {
option.style.background = '#007acc';
});
option.addEventListener('mouseleave', () => {
option.style.background = '';
});
option.addEventListener('click', () => {
condition.node_type = nodeType;
updateSelectedDisplay();
updateAttributeOptions(nodeType);
dropdownList.style.display = 'none';
searchInput.style.display = 'none';
selectedDisplay.style.display = 'flex';
});
dropdownList.appendChild(option);
});
dropdownList.style.display = filtered.length > 0 ? 'block' : 'none';
};
selectedDisplay.addEventListener('click', () => {
selectedDisplay.style.display = 'none';
searchInput.style.display = 'block';
searchInput.focus();
filterDropdown('');
});
searchInput.addEventListener('input', () => {
filterDropdown(searchInput.value);
});
searchInput.addEventListener('blur', () => {
setTimeout(() => {
searchInput.style.display = 'none';
selectedDisplay.style.display = 'flex';
dropdownList.style.display = 'none';
}, 200);
});
selectContainer.appendChild(selectedDisplay);
selectContainer.appendChild(searchInput);
selectContainer.appendChild(dropdownList);
nodeTypeContainer.appendChild(nodeTypeLabel);
nodeTypeContainer.appendChild(selectContainer);
// Attribute name dropdown (populated based on selected node)
const attrNameContainer = document.createElement('div');
attrNameContainer.style.marginBottom = '10px';
const attrNameLabel = document.createElement('label');
attrNameLabel.textContent = 'Attribute Name: ';
attrNameLabel.style.display = 'block';
attrNameLabel.style.marginBottom = '5px';
const attrNameSelect = document.createElement('select');
attrNameSelect.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
`;
// Function to update attribute dropdown based on selected node type
const updateAttributeOptions = (nodeType) => {
attrNameSelect.innerHTML = '';
// Add empty option
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = '-- Select Attribute --';
attrNameSelect.appendChild(emptyOption);
if (nodeType && this.nodeTypes[nodeType]) {
const nodeInfo = this.nodeTypes[nodeType];
const inputs = nodeInfo.input?.required || {};
const optionalInputs = nodeInfo.input?.optional || {};
// Add required inputs
for (const [inputName, inputDef] of Object.entries(inputs)) {
const option = document.createElement('option');
option.value = inputName;
option.textContent = inputName;
if (condition.attribute_name === inputName) {
option.selected = true;
}
attrNameSelect.appendChild(option);
}
// Add optional inputs
for (const [inputName, inputDef] of Object.entries(optionalInputs)) {
const option = document.createElement('option');
option.value = inputName;
option.textContent = `${inputName} (optional)`;
if (condition.attribute_name === inputName) {
option.selected = true;
}
attrNameSelect.appendChild(option);
}
}
};
attrNameSelect.addEventListener('change', () => {
condition.attribute_name = attrNameSelect.value;
});
// Initialize displays
updateSelectedDisplay();
updateAttributeOptions(condition.node_type);
attrNameContainer.appendChild(attrNameLabel);
attrNameContainer.appendChild(attrNameSelect);
// Attribute values
const attrValuesContainer = document.createElement('div');
attrValuesContainer.style.marginBottom = '10px';
const attrValuesLabel = document.createElement('label');
attrValuesLabel.textContent = 'Attribute Values (comma-separated): ';
attrValuesLabel.style.display = 'block';
attrValuesLabel.style.marginBottom = '5px';
const attrValuesInput = document.createElement('input');
attrValuesInput.type = 'text';
attrValuesInput.value = (condition.attribute_values || []).join(', ');
attrValuesInput.placeholder = 'e.g., model1.safetensors, model2.ckpt';
attrValuesInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
`;
attrValuesInput.addEventListener('input', () => {
condition.attribute_values = attrValuesInput.value.split(',').map(s => s.trim()).filter(s => s);
});
attrValuesContainer.appendChild(attrValuesLabel);
attrValuesContainer.appendChild(attrValuesInput);
container.appendChild(nodeTypeContainer);
container.appendChild(attrNameContainer);
container.appendChild(attrValuesContainer);
}
createWorkflowNameFields(container, condition) {
const label = document.createElement('label');
label.textContent = 'Workflow Names (comma-separated): ';
label.style.display = 'block';
label.style.marginBottom = '5px';
const input = document.createElement('input');
input.type = 'text';
input.value = (condition.workflow_names || []).join(', ');
input.placeholder = 'e.g., portrait, landscape, character';
input.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
`;
input.addEventListener('input', () => {
condition.workflow_names = input.value.split(',').map(s => s.trim()).filter(s => s);
});
container.appendChild(label);
container.appendChild(input);
}
createNodeInWorkflowFields(container, condition) {
const label = document.createElement('label');
label.textContent = 'Node Types: ';
label.style.display = 'block';
label.style.marginBottom = '5px';
// Multi-select dropdown with searchable autocomplete
const selectContainer = document.createElement('div');
selectContainer.style.cssText = `
position: relative;
width: 100%;
`;
// Create a searchable input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search node types...';
searchInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
margin-bottom: 5px;
`;
// Selected items display
const selectedDisplay = document.createElement('div');
selectedDisplay.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
min-height: 30px;
padding: 5px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
`;
// Dropdown list
const dropdownList = document.createElement('div');
dropdownList.style.cssText = `
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
z-index: 1000;
display: none;
`;
// Get node types from workflow
const workflowNodeTypes = new Set();
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
workflowNodeTypes.add(node.type);
}
}
// Populate dropdown with all node types
const nodeTypesList = Object.keys(this.nodeTypes).sort();
const updateSelectedDisplay = () => {
selectedDisplay.innerHTML = '';
(condition.node_types || []).forEach(nodeType => {
const tag = document.createElement('span');
tag.style.cssText = `
background: #007acc;
color: #fff;
padding: 3px 8px;
border-radius: 3px;
display: inline-flex;
align-items: center;
gap: 5px;
`;
// Add icon if in workflow
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
tag.appendChild(icon);
}
const nameSpan = document.createElement('span');
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
tag.appendChild(nameSpan);
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.cssText = `
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 0 3px;
font-size: 16px;
`;
removeBtn.addEventListener('click', () => {
condition.node_types = condition.node_types.filter(t => t !== nodeType);
updateSelectedDisplay();
});
tag.appendChild(removeBtn);
selectedDisplay.appendChild(tag);
});
};
const filterDropdown = (searchTerm) => {
dropdownList.innerHTML = '';
const lowerSearch = searchTerm.toLowerCase();
const filtered = nodeTypesList.filter(nodeType => {
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
return displayName.toLowerCase().includes(lowerSearch) &&
!(condition.node_types || []).includes(nodeType);
});
if (filtered.length === 0) {
dropdownList.style.display = 'none';
return;
}
filtered.forEach(nodeType => {
const option = document.createElement('div');
option.style.cssText = `
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #555;
display: flex;
align-items: center;
gap: 5px;
`;
// Add icon if in workflow
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
option.appendChild(icon);
}
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
const textSpan = document.createElement('span');
textSpan.textContent = displayName;
option.appendChild(textSpan);
option.addEventListener('mouseover', () => {
option.style.background = '#007acc';
});
option.addEventListener('mouseout', () => {
option.style.background = '';
});
option.addEventListener('click', () => {
condition.node_types = condition.node_types || [];
condition.node_types.push(nodeType);
searchInput.value = '';
updateSelectedDisplay();
filterDropdown('');
searchInput.focus();
});
dropdownList.appendChild(option);
});
dropdownList.style.display = 'block';
};
searchInput.addEventListener('focus', () => {
filterDropdown(searchInput.value);
});
searchInput.addEventListener('input', () => {
filterDropdown(searchInput.value);
});
searchInput.addEventListener('blur', () => {
setTimeout(() => {
dropdownList.style.display = 'none';
}, 200);
});
updateSelectedDisplay();
selectContainer.appendChild(searchInput);
selectContainer.appendChild(dropdownList);
container.appendChild(label);
container.appendChild(selectedDisplay);
container.appendChild(selectContainer);
}
createAttributeInWorkflowFields(container, condition) {
const nodeTypeLabel = document.createElement('label');
nodeTypeLabel.textContent = 'Node Type: ';
nodeTypeLabel.style.display = 'block';
nodeTypeLabel.style.marginBottom = '5px';
// Node Type dropdown with search
const nodeTypeContainer = document.createElement('div');
nodeTypeContainer.style.cssText = `
position: relative;
width: 100%;
margin-bottom: 10px;
`;
const nodeTypeInput = document.createElement('input');
nodeTypeInput.type = 'text';
nodeTypeInput.placeholder = 'Search node type...';
nodeTypeInput.value = condition.node_type ? (this.nodeTypes[condition.node_type]?.display_name || condition.node_type) : '';
nodeTypeInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
`;
const nodeTypeDropdown = document.createElement('div');
nodeTypeDropdown.style.cssText = `
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
z-index: 1000;
display: none;
`;
// Get node types from workflow
const workflowNodeTypes = new Set();
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
workflowNodeTypes.add(node.type);
}
}
const nodeTypesList = Object.keys(this.nodeTypes).sort();
const filterNodeTypeDropdown = (searchTerm) => {
nodeTypeDropdown.innerHTML = '';
const lowerSearch = searchTerm.toLowerCase();
const filtered = nodeTypesList.filter(nodeType => {
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
return displayName.toLowerCase().includes(lowerSearch);
});
if (filtered.length === 0) {
nodeTypeDropdown.style.display = 'none';
return;
}
filtered.forEach(nodeType => {
const option = document.createElement('div');
option.style.cssText = `
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #555;
display: flex;
align-items: center;
gap: 5px;
`;
// Add icon if in workflow
if (workflowNodeTypes.has(nodeType)) {
const icon = document.createElement('span');
icon.textContent = '✓';
icon.style.color = '#0f0';
option.appendChild(icon);
}
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
const textSpan = document.createElement('span');
textSpan.textContent = displayName;
option.appendChild(textSpan);
option.addEventListener('mouseover', () => {
option.style.background = '#007acc';
});
option.addEventListener('mouseout', () => {
option.style.background = '';
});
option.addEventListener('click', () => {
condition.node_type = nodeType;
nodeTypeInput.value = displayName;
nodeTypeDropdown.style.display = 'none';
// Refresh attribute dropdown
updateAttributeDropdown();
});
nodeTypeDropdown.appendChild(option);
});
nodeTypeDropdown.style.display = 'block';
};
nodeTypeInput.addEventListener('focus', () => {
filterNodeTypeDropdown(nodeTypeInput.value);
});
nodeTypeInput.addEventListener('input', () => {
filterNodeTypeDropdown(nodeTypeInput.value);
});
nodeTypeInput.addEventListener('blur', () => {
setTimeout(() => {
nodeTypeDropdown.style.display = 'none';
}, 200);
});
nodeTypeContainer.appendChild(nodeTypeInput);
nodeTypeContainer.appendChild(nodeTypeDropdown);
// Attribute Name dropdown
const attrLabel = document.createElement('label');
attrLabel.textContent = 'Attribute Name: ';
attrLabel.style.display = 'block';
attrLabel.style.marginBottom = '5px';
attrLabel.style.marginTop = '10px';
const attrSelect = document.createElement('select');
attrSelect.style.cssText = `
width: 100%;
padding: 5px;
background: #2a2a2a;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
margin-bottom: 10px;
`;
const updateAttributeDropdown = () => {
attrSelect.innerHTML = '<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,
tags: this.currentTags || [],
trigger_conditions: cleanedTriggerConditions
};
try {
const response = await api.fetchApi(`/autonotes/notes/${this.currentEditingNote.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.success) {
// Update local copy
Object.assign(this.currentEditingNote, data);
await this.refreshNotes();
return true;
} else {
alert('Failed to save note');
return false;
}
} catch (error) {
console.error('Failed to save note:', error);
alert('Failed to save note');
return false;
}
}
async closeEditDialog(save = false) {
// Always auto-save if there are unsaved changes
if (this.currentEditingNote && this.textareaChanged) {
await this.saveCurrentNote();
this.textareaChanged = false;
} else if (save && this.currentEditingNote) {
await this.saveCurrentNote();
}
// Remove global auto-save handler
if (this.globalAutoSaveHandler) {
document.removeEventListener('click', this.globalAutoSaveHandler, true);
this.globalAutoSaveHandler = null;
}
if (this.editDialog) {
this.editDialog.parentElement.remove(); // Remove overlay
this.editDialog = null;
this.currentEditingNote = null;
}
}
async togglePin(note) {
try {
const newPinnedState = !note.pinned;
const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: note.content,
format_style: note.format_style,
pinned: newPinnedState,
trigger_conditions: note.trigger_conditions || []
}),
});
const result = await response.json();
if (result.success) {
note.pinned = newPinnedState;
await this.refreshNotes();
} else {
alert('Failed to toggle pin');
}
} catch (error) {
console.error('Failed to toggle pin:', error);
alert('Failed to toggle pin');
}
}
async addFolder(treeContainer, selectedNoteUuid) {
const name = prompt('Enter folder name:');
if (!name) return;
try {
const response = await api.fetchApi('/autonotes/folders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
const result = await response.json();
if (result.uuid) {
await this.loadFolders();
this.renderNoteTree(treeContainer, selectedNoteUuid);
}
} catch (error) {
console.error('Failed to create folder:', error);
alert('Failed to create folder');
}
}
async renameFolder(folder, treeContainer, selectedNoteUuid) {
const newName = prompt('Enter new folder name:', folder.name);
if (!newName || newName === folder.name) return;
try {
const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newName }),
});
const result = await response.json();
if (result.success) {
await this.loadFolders();
this.renderNoteTree(treeContainer, selectedNoteUuid);
} else {
alert('Failed to rename folder');
}
} catch (error) {
console.error('Failed to rename folder:', error);
alert('Failed to rename folder');
}
}
async deleteFolder(folder, treeContainer, selectedNoteUuid) {
if (!confirm(`Delete folder "${folder.name}"? Notes inside will not be deleted.`)) {
return;
}
try {
const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, {
method: 'DELETE',
});
const result = await response.json();
if (result.success) {
await this.loadFolders();
this.renderNoteTree(treeContainer, selectedNoteUuid);
} else {
alert('Failed to delete folder');
}
} catch (error) {
console.error('Failed to delete folder:', error);
alert('Failed to delete folder');
}
}
async moveNoteToFolder(noteUuid, folderUuid, treeContainer, selectedNoteUuid) {
try {
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder_uuid: folderUuid }),
});
const result = await response.json();
if (result.success) {
await this.refreshNotes();
this.renderNoteTree(treeContainer, selectedNoteUuid);
} else {
alert('Failed to move note');
}
} catch (error) {
console.error('Failed to move note:', error);
alert('Failed to move note');
}
}
async moveFolderToFolder(folderUuid, parentFolderUuid, treeContainer, selectedNoteUuid) {
try {
// Prevent circular references
if (folderUuid === parentFolderUuid) {
alert('Cannot move a folder into itself');
return;
}
// Check if parent is a descendant of the folder being moved
const isDescendant = (checkFolderId, ancestorId) => {
if (!checkFolderId) return false;
if (checkFolderId === ancestorId) return true;
const folder = this.folders.find(f => f.uuid === checkFolderId);
if (!folder) return false;
return isDescendant(folder.parent_uuid, ancestorId);
};
if (isDescendant(parentFolderUuid, folderUuid)) {
alert('Cannot move a folder into its own descendant');
return;
}
const response = await api.fetchApi(`/autonotes/folders/${folderUuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ parent_uuid: parentFolderUuid }),
});
const result = await response.json();
if (result.success) {
await this.loadFolders();
this.renderNoteTree(treeContainer, selectedNoteUuid);
} else {
alert('Failed to move folder');
}
} catch (error) {
console.error('Failed to move folder:', error);
alert('Failed to move folder');
}
}
showFolderContextMenu(event, folder, treeContainer, selectedNoteUuid) {
// Remove any existing context menu
const existingMenu = document.querySelector('.folder-context-menu');
if (existingMenu) {
existingMenu.remove();
}
const menu = document.createElement('div');
menu.className = 'folder-context-menu';
menu.style.cssText = `
position: fixed;
left: ${event.clientX}px;
top: ${event.clientY}px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 4px;
padding: 5px 0;
z-index: 10000;
min-width: 150px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
`;
const createChildOption = document.createElement('div');
createChildOption.textContent = 'New Child Folder';
createChildOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
createChildOption.addEventListener('mouseenter', () => {
createChildOption.style.background = '#007acc';
});
createChildOption.addEventListener('mouseleave', () => {
createChildOption.style.background = '';
});
createChildOption.addEventListener('click', async () => {
menu.remove();
const folderName = prompt('Enter folder name:');
if (folderName) {
await this.createFolderWithParent(folderName, folder.uuid, treeContainer, selectedNoteUuid);
}
});
const renameOption = document.createElement('div');
renameOption.textContent = 'Rename';
renameOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
renameOption.addEventListener('mouseenter', () => {
renameOption.style.background = '#007acc';
});
renameOption.addEventListener('mouseleave', () => {
renameOption.style.background = '';
});
renameOption.addEventListener('click', async () => {
menu.remove();
this.renameFolder(folder, treeContainer, selectedNoteUuid);
});
const deleteOption = document.createElement('div');
deleteOption.textContent = 'Delete';
deleteOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #f88;
`;
deleteOption.addEventListener('mouseenter', () => {
deleteOption.style.background = '#007acc';
});
deleteOption.addEventListener('mouseleave', () => {
deleteOption.style.background = '';
});
deleteOption.addEventListener('click', async () => {
menu.remove();
this.deleteFolder(folder, treeContainer, selectedNoteUuid);
});
menu.appendChild(createChildOption);
menu.appendChild(renameOption);
menu.appendChild(deleteOption);
document.body.appendChild(menu);
// Close menu when clicking outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
}
async createFolderWithParent(name, parentUuid, treeContainer, selectedNoteUuid) {
try {
const response = await api.fetchApi('/autonotes/folders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, parent_uuid: parentUuid }),
});
const result = await response.json();
if (result.uuid) {
await this.loadFolders();
// Expand the parent folder
this.expandedFolders.add(parentUuid);
this.renderNoteTree(treeContainer, selectedNoteUuid);
}
} catch (error) {
console.error('Failed to create folder:', error);
alert('Failed to create folder');
}
}
showNoteContextMenu(event, note, treeContainer, selectedNoteUuid) {
// Remove any existing context menu
const existingMenu = document.querySelector('.note-context-menu');
if (existingMenu) {
existingMenu.remove();
}
const menu = document.createElement('div');
menu.className = 'note-context-menu';
menu.style.cssText = `
position: fixed;
left: ${event.clientX}px;
top: ${event.clientY}px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 4px;
padding: 5px 0;
z-index: 10000;
min-width: 150px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
`;
const renameOption = document.createElement('div');
renameOption.textContent = 'Rename';
renameOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
renameOption.addEventListener('mouseenter', () => {
renameOption.style.background = '#007acc';
});
renameOption.addEventListener('mouseleave', () => {
renameOption.style.background = '';
});
renameOption.addEventListener('click', async () => {
menu.remove();
const newName = prompt('Enter new note name:', note.name);
if (newName && newName !== note.name) {
await this.renameNote(note.uuid, newName, treeContainer, selectedNoteUuid);
}
});
const duplicateOption = document.createElement('div');
duplicateOption.textContent = 'Duplicate';
duplicateOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
duplicateOption.addEventListener('mouseenter', () => {
duplicateOption.style.background = '#007acc';
});
duplicateOption.addEventListener('mouseleave', () => {
duplicateOption.style.background = '';
});
duplicateOption.addEventListener('click', async () => {
menu.remove();
const newName = prompt('Enter name for duplicated note:', note.name);
if (newName) {
await this.duplicateNote(note.uuid, newName, treeContainer, selectedNoteUuid);
}
});
const deleteOption = document.createElement('div');
deleteOption.textContent = 'Delete';
deleteOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
deleteOption.addEventListener('mouseenter', () => {
deleteOption.style.background = '#cc0000';
});
deleteOption.addEventListener('mouseleave', () => {
deleteOption.style.background = '';
});
deleteOption.addEventListener('click', async () => {
menu.remove();
if (confirm(`Delete note "${note.name}"?`)) {
await this.deleteNote(note.uuid, treeContainer, selectedNoteUuid);
}
});
menu.appendChild(renameOption);
menu.appendChild(duplicateOption);
menu.appendChild(deleteOption);
document.body.appendChild(menu);
// Close menu when clicking outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => document.addEventListener('click', closeMenu), 0);
}
async renameNote(noteUuid, newName, treeContainer, selectedNoteUuid) {
try {
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newName }),
});
const result = await response.json();
if (result.success) {
await this.refreshNotes();
this.renderNoteTree(treeContainer, selectedNoteUuid);
} else {
alert('Failed to rename note');
}
} catch (error) {
console.error('Failed to rename note:', error);
alert('Failed to rename note');
}
}
async deleteNote(noteUuid, treeContainer, selectedNoteUuid) {
try {
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
method: 'DELETE',
});
const result = await response.json();
if (result.success) {
await this.refreshNotes();
this.renderNoteTree(treeContainer, selectedNoteUuid);
// Clear editor if deleted note was being edited
if (this.currentEditingNote && this.currentEditingNote.uuid === noteUuid) {
this.currentEditingNote = null;
if (this.contentTextarea) {
this.contentTextarea.value = '';
}
}
} else {
alert('Failed to delete note');
}
} catch (error) {
console.error('Failed to delete note:', error);
alert('Failed to delete note');
}
}
async duplicateNote(noteUuid, newName, treeContainer, selectedNoteUuid) {
try {
// Get the original note
const originalNote = this.notes.find(n => n.uuid === noteUuid);
if (!originalNote) {
alert('Note not found');
return;
}
// Create a new note with the same folder
const response = await api.fetchApi('/autonotes/notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newName,
folder_uuid: originalNote.folder_uuid
}),
});
const noteResult = await response.json();
if (noteResult.uuid) {
// Update the new note with all properties from the original
await api.fetchApi(`/autonotes/notes/${noteResult.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: originalNote.content,
format_style: originalNote.format_style,
pinned: false, // Don't duplicate the pinned state
tags: [...(originalNote.tags || [])],
trigger_conditions: [...(originalNote.trigger_conditions || [])]
}),
});
await this.refreshNotes();
if (treeContainer) {
this.renderNoteTree(treeContainer, selectedNoteUuid);
}
}
} catch (error) {
console.error('Failed to duplicate note:', error);
alert('Failed to duplicate note');
}
}
showSidebarNoteContextMenu(event, note) {
// Remove any existing context menu
const existingMenu = document.querySelector('.sidebar-note-context-menu');
if (existingMenu) {
existingMenu.remove();
}
const menu = document.createElement('div');
menu.className = 'sidebar-note-context-menu';
menu.style.cssText = `
position: fixed;
left: ${event.clientX}px;
top: ${event.clientY}px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 4px;
padding: 5px 0;
z-index: 10000;
min-width: 150px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
`;
const editOption = document.createElement('div');
editOption.textContent = 'Edit';
editOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
editOption.addEventListener('mouseenter', () => {
editOption.style.background = '#007acc';
});
editOption.addEventListener('mouseleave', () => {
editOption.style.background = '';
});
editOption.addEventListener('click', async () => {
menu.remove();
await this.openEditDialog(note.uuid);
});
const duplicateOption = document.createElement('div');
duplicateOption.textContent = 'Duplicate';
duplicateOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #fff;
`;
duplicateOption.addEventListener('mouseenter', () => {
duplicateOption.style.background = '#007acc';
});
duplicateOption.addEventListener('mouseleave', () => {
duplicateOption.style.background = '';
});
duplicateOption.addEventListener('click', async () => {
menu.remove();
const newName = prompt('Enter name for duplicated note:', note.name);
if (newName) {
await this.duplicateNote(note.uuid, newName, null, null);
}
});
const deleteOption = document.createElement('div');
deleteOption.textContent = 'Delete';
deleteOption.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: #f88;
`;
deleteOption.addEventListener('mouseenter', () => {
deleteOption.style.background = '#cc0000';
});
deleteOption.addEventListener('mouseleave', () => {
deleteOption.style.background = '';
});
deleteOption.addEventListener('click', async () => {
menu.remove();
if (confirm(`Delete note "${note.name}"?`)) {
try {
const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, {
method: 'DELETE',
});
const result = await response.json();
if (result.success) {
await this.refreshNotes();
} else {
alert('Failed to delete note');
}
} catch (error) {
console.error('Failed to delete note:', error);
alert('Failed to delete note');
}
}
});
menu.appendChild(editOption);
menu.appendChild(duplicateOption);
menu.appendChild(deleteOption);
document.body.appendChild(menu);
// Close menu when clicking outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => document.addEventListener('click', closeMenu), 0);
}
async updateNoteContent(noteUuid, content) {
try {
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content }),
});
const result = await response.json();
if (result.success) {
// Refresh to show updated content
await this.refreshNotes();
} else {
alert('Failed to save note');
}
} catch (error) {
console.error('Failed to save note:', error);
alert('Failed to save note');
}
}
async updateNoteTags(note) {
try {
const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: note.content,
format_style: note.format_style,
pinned: note.pinned,
tags: note.tags || [],
trigger_conditions: note.trigger_conditions || []
}),
});
const result = await response.json();
if (result.success) {
await this.refreshNotes();
} else {
alert('Failed to update tags');
}
} catch (error) {
console.error('Failed to update tags:', error);
alert('Failed to update tags');
}
}
}
// Initialize the AutoNotes manager when the extension loads
let autoNotesManagerInstance = null;
app.registerExtension({
name: "AutoNotes",
async setup() {
autoNotesManagerInstance = new AutoNotesManager();
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
const menuOptions = getExtraMenuOptions?.apply(this, arguments);
options.unshift({
content: "Create AutoNote for this node",
callback: async () => {
if (autoNotesManagerInstance) {
// Set the selected node type to this node's type
autoNotesManagerInstance.selectedNodeType = this.type;
// Collect node attributes
const nodeAttributes = {};
if (this.widgets) {
for (const widget of this.widgets) {
nodeAttributes[widget.name] = widget.value;
}
}
autoNotesManagerInstance.selectedNodeAttributes = nodeAttributes;
// Call the add note from current node function
await autoNotesManagerInstance.addNoteFromCurrentNode();
}
}
});
return menuOptions;
};
}
});