commit ddd6584da9f24ee2915341e318ec5677eca7d605 Author: John Cantu Date: Mon Sep 29 23:05:02 2025 -0700 Initial commit diff --git a/.github/workflows/publish_action.yml b/.github/workflows/publish_action.yml new file mode 100644 index 0000000..122a1ae --- /dev/null +++ b/.github/workflows/publish_action.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fedfe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# OSX useful to ignore +*.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# IntelliJ Idea +.idea +*.iml +*.ipr +*.iws + +# PyBuilder +target/ + +# Cookiecutter +output/ +python_boilerplate/ +cookiecutter-pypackage-env/ + +# vscode settings +.history/ +*.code-workspace + +# Frontend extension +node_modules/ +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +node.zip +.vscode/ +.claude/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..004983b --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# ComfyUI-AutoNotes + +An intelligent note-taking extension for ComfyUI that automatically displays relevant notes based on your workflow context. + +## Features + +- **Smart Note Display**: Notes automatically appear based on trigger conditions like selected nodes, node attributes, or workflow names +- **Sidebar Interface**: Non-intrusive sidebar that doesn't interfere with your workflow +- **Multiple Display Modes**: + - "All" mode shows all notes + - "Automatic" mode shows only relevant notes based on triggers +- **Rich Formatting**: Support for both plaintext and Markdown formatting +- **Trigger Conditions**: Configure when notes should appear: + - **Node Selected**: Show notes when specific node types are selected + - **Node Attribute**: Show notes when nodes have specific attribute values + - **Workflow Name**: Show notes when workflow filename contains specific text +- **Pinnable Notes**: Pin important notes to always display regardless of context +- **Organized Storage**: Notes stored in your ComfyUI user directory with UUID-based organization +- **Edit Dialog**: Full-featured editor with tree view and trigger condition management + +## Installation + +1. Clone or download this repository into your ComfyUI `custom_nodes` directory: + + ``` + cd ComfyUI/custom_nodes + git clone https://github.com/yourusername/ComfyUI-AutoNotes.git + ``` + +2. Restart ComfyUI + +3. The AutoNotes sidebar will appear on the right side of your ComfyUI interface + +## Usage + +### Basic Operations + +1. **Creating Notes**: Click the "Add" button in the sidebar control panel +2. **Editing Notes**: Click the "Edit" button to open the full editor dialog +3. **Display Modes**: Switch between "All" and "Automatic" in the dropdown + +### Setting Up Trigger Conditions + +In the Edit dialog, you can configure when notes should automatically appear: + +#### Node Selected Trigger + +- Displays the note when any of the specified node types is selected +- Example: `CheckpointLoaderSimple, LoraLoader` + +#### Node Attribute Trigger + +- Displays the note when a specific node has certain attribute values +- Useful for showing notes about specific models or settings +- Example: + - Node Type: `CheckpointLoaderSimple` + - Attribute Name: `ckpt_name` + - Attribute Values: `mymodel.safetensors, specialmodel.ckpt` + +#### Workflow Name Trigger + +- Displays the note when the workflow filename contains specific text +- Example: `portrait, character, anime` + +### Example Use Cases + +1. **Model-Specific Notes**: Create notes with tips about specific checkpoints that appear when you select them +2. **Node Documentation**: Add usage instructions that appear when you select certain node types +3. **Workflow Reminders**: Set up notes that remind you of important settings for specific types of workflows +4. **Quick Reference**: Pin frequently-used information to always stay visible + +### Data Storage + +Notes are stored in your ComfyUI user directory under `autonotes/`: + +- `notes.json`: Contains all note data and metadata +- `folders.json`: Contains folder organization (future feature) + +## Technical Details + +### File Structure + +```text +ComfyUI-AutoNotes/ +├── __init__.py # Extension entry point +├── autonotes.py # Main Python backend +├── web/ +│ └── autonotes.js # Frontend JavaScript +├── README.md # This file +└── requirements.md # Original requirements document +``` + +### API Endpoints + +The extension exposes REST API endpoints for note management: + +- `GET /autonotes/notes` - Retrieve notes with filtering +- `POST /autonotes/notes` - Create new note +- `PUT /autonotes/notes/{uuid}` - Update existing note +- `DELETE /autonotes/notes/{uuid}` - Delete note +- `GET /autonotes/folders` - Retrieve folders +- `POST /autonotes/folders` - Create new folder + +### Data Models + +**Note Structure**: + +```json +{ + "uuid": "unique-identifier", + "folder_uuid": "parent-folder-id", + "name": "Note Title", + "content": "Note content...", + "format_style": "plaintext|markdown", + "pinned": true|false, + "trigger_conditions": [...] +} +``` + +**Trigger Condition Structure**: + +```json +{ + "type": "node_selected|node_attribute|workflow_name", + "node_types": ["NodeType1", "NodeType2"], + "node_type": "SpecificNodeType", + "attribute_name": "attribute_name", + "attribute_values": ["value1", "value2"], + "workflow_names": ["name1", "name2"] +} +``` + +## Development + +The extension follows ComfyUI's standard custom node structure: + +- Python backend handles data management and API endpoints +- JavaScript frontend provides the user interface +- Uses ComfyUI's built-in web server and extension system + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly with ComfyUI +5. Submit a pull request diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cbb5d2e --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .autonotes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, WEB_DIRECTORY + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file diff --git a/autonotes.py b/autonotes.py new file mode 100644 index 0000000..19ae2b8 --- /dev/null +++ b/autonotes.py @@ -0,0 +1,409 @@ +import os +import json +import uuid +from typing import Dict, List, Optional, Any +from dataclasses import dataclass, asdict +import folder_paths +from server import PromptServer +from aiohttp import web + + +@dataclass +class TriggerCondition: + type: str # "node_selected", "node_attribute", "workflow_name" + node_types: Optional[List[str]] = None + node_type: Optional[str] = None + attribute_name: Optional[str] = None + attribute_values: Optional[List[str]] = None + workflow_names: Optional[List[str]] = None + + +@dataclass +class Note: + uuid: str + folder_uuid: Optional[str] + content: str + format_style: str # "plaintext" or "markdown" + trigger_conditions: List[TriggerCondition] + pinned: bool = False + name: str = "" + + +@dataclass +class Folder: + uuid: str + parent_uuid: Optional[str] + name: str + + +class AutoNotesManager: + def __init__(self): + self.user_dir = folder_paths.get_user_directory() + self.data_dir = os.path.join(self.user_dir, "autonotes") + self.notes_file = os.path.join(self.data_dir, "notes.json") + self.folders_file = os.path.join(self.data_dir, "folders.json") + + # Ensure data directory exists + os.makedirs(self.data_dir, exist_ok=True) + + # Load existing data + self.notes: Dict[str, Note] = self._load_notes() + self.folders: Dict[str, Folder] = self._load_folders() + + def _load_notes(self) -> Dict[str, Note]: + if os.path.exists(self.notes_file): + try: + with open(self.notes_file, 'r', encoding='utf-8') as f: + data = json.load(f) + notes = {} + for note_data in data: + # Convert trigger conditions + trigger_conditions = [] + for tc_data in note_data.get('trigger_conditions', []): + trigger_conditions.append(TriggerCondition(**tc_data)) + + note_data['trigger_conditions'] = trigger_conditions + note = Note(**note_data) + notes[note.uuid] = note + return notes + except Exception as e: + print(f"Error loading notes: {e}") + return {} + + def _load_folders(self) -> Dict[str, Folder]: + if os.path.exists(self.folders_file): + try: + with open(self.folders_file, 'r', encoding='utf-8') as f: + data = json.load(f) + folders = {} + for folder_data in data: + folder = Folder(**folder_data) + folders[folder.uuid] = folder + return folders + except Exception as e: + print(f"Error loading folders: {e}") + return {} + + def _save_notes(self): + try: + data = [] + for note in self.notes.values(): + note_dict = asdict(note) + # Convert trigger conditions to dict + note_dict['trigger_conditions'] = [asdict(tc) for tc in note.trigger_conditions] + data.append(note_dict) + + with open(self.notes_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Error saving notes: {e}") + + def _save_folders(self): + try: + data = [asdict(folder) for folder in self.folders.values()] + with open(self.folders_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Error saving folders: {e}") + + def create_note(self, name: str, folder_uuid: Optional[str] = None) -> str: + note_uuid = str(uuid.uuid4()) + note = Note( + uuid=note_uuid, + folder_uuid=folder_uuid, + content="", + format_style="plaintext", + trigger_conditions=[], + name=name + ) + self.notes[note_uuid] = note + self._save_notes() + return note_uuid + + def update_note(self, note_uuid: str, **kwargs) -> bool: + if note_uuid not in self.notes: + return False + + note = self.notes[note_uuid] + for key, value in kwargs.items(): + if hasattr(note, key): + setattr(note, key, value) + + self._save_notes() + return True + + def delete_note(self, note_uuid: str) -> bool: + if note_uuid in self.notes: + del self.notes[note_uuid] + self._save_notes() + return True + return False + + def create_folder(self, name: str, parent_uuid: Optional[str] = None) -> str: + folder_uuid = str(uuid.uuid4()) + folder = Folder( + uuid=folder_uuid, + parent_uuid=parent_uuid, + name=name + ) + self.folders[folder_uuid] = folder + self._save_folders() + return folder_uuid + + def get_notes_for_display(self, mode: str = "all", selected_node_type: Optional[str] = None, + selected_node_attributes: Optional[Dict[str, Any]] = None, + workflow_name: Optional[str] = None, + workflow_nodes: Optional[Dict[str, Any]] = None) -> List[Note]: + if mode == "all": + return list(self.notes.values()) + + # Automatic mode - filter by trigger conditions + matching_notes = [] + + for note in self.notes.values(): + if note.pinned: + matching_notes.append(note) + continue + + for condition in note.trigger_conditions: + if self._check_trigger_condition(condition, selected_node_type, + selected_node_attributes, workflow_name, + workflow_nodes): + matching_notes.append(note) + break + + return matching_notes + + def _check_trigger_condition(self, condition: TriggerCondition, + selected_node_type: Optional[str], + selected_node_attributes: Optional[Dict[str, Any]], + workflow_name: Optional[str], + workflow_nodes: Optional[Dict[str, Any]] = None) -> bool: + if condition.type == "node_selected": + return (selected_node_type is not None and + condition.node_types is not None and + selected_node_type in condition.node_types) + + elif condition.type == "node_attribute": + if (selected_node_type == condition.node_type and + selected_node_attributes is not None and + condition.attribute_name in selected_node_attributes and + condition.attribute_values is not None): + + attr_value = str(selected_node_attributes[condition.attribute_name]) + return any(val in attr_value for val in condition.attribute_values) + + elif condition.type == "node_in_workflow": + if workflow_nodes is not None and condition.node_types is not None: + # Check if any of the specified node types exist in the workflow + return any(node_type in workflow_nodes for node_type in condition.node_types) + + elif condition.type == "attribute_in_workflow": + if (workflow_nodes is not None and + condition.node_type is not None and + condition.attribute_name is not None): + + # Check if the node type exists and has the specified attribute + if condition.node_type in workflow_nodes: + node_attrs = workflow_nodes[condition.node_type] + if condition.attribute_name in node_attrs: + # If attribute_values is specified, check if any match + if condition.attribute_values is not None and len(condition.attribute_values) > 0: + attr_value = str(node_attrs[condition.attribute_name]) + return any(val in attr_value for val in condition.attribute_values) + else: + # No specific values required, just presence is enough + return True + + elif condition.type == "workflow_name": + if workflow_name is not None and condition.workflow_names is not None: + return any(name in workflow_name for name in condition.workflow_names) + + return False + + +# Global manager instance +autonotes_manager = AutoNotesManager() + + +class AutoNotesNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": {}, + } + + RETURN_TYPES = () + FUNCTION = "process" + OUTPUT_NODE = True + CATEGORY = "AutoNotes" + + def process(self): + # This node doesn't actually process anything in the workflow + # It's just here to provide a presence in the node list + return {} + + +# Web API routes +@PromptServer.instance.routes.get("/autonotes/notes") +async def get_notes(request): + try: + mode = request.query.get('mode', 'all') + selected_node_type = request.query.get('node_type') + workflow_name = request.query.get('workflow_name') + + # Parse node attributes if provided + selected_node_attributes = None + if 'node_attributes' in request.query: + try: + selected_node_attributes = json.loads(request.query['node_attributes']) + except: + pass + + # Parse workflow nodes if provided + workflow_nodes = None + if 'workflow_nodes' in request.query: + try: + workflow_nodes = json.loads(request.query['workflow_nodes']) + except: + pass + + notes = autonotes_manager.get_notes_for_display( + mode=mode, + selected_node_type=selected_node_type, + selected_node_attributes=selected_node_attributes, + workflow_name=workflow_name, + workflow_nodes=workflow_nodes + ) + + # Convert notes to JSON-serializable format + notes_data = [] + for note in notes: + note_dict = asdict(note) + note_dict['trigger_conditions'] = [asdict(tc) for tc in note.trigger_conditions] + notes_data.append(note_dict) + + return web.json_response(notes_data) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.post("/autonotes/notes") +async def create_note(request): + try: + data = await request.json() + name = data.get('name', 'New Note') + folder_uuid = data.get('folder_uuid') + + note_uuid = autonotes_manager.create_note(name, folder_uuid) + return web.json_response({"uuid": note_uuid}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.put("/autonotes/notes/{note_uuid}") +async def update_note(request): + try: + note_uuid = request.match_info['note_uuid'] + data = await request.json() + + # Handle trigger conditions conversion + if 'trigger_conditions' in data: + trigger_conditions = [] + for tc_data in data['trigger_conditions']: + trigger_conditions.append(TriggerCondition(**tc_data)) + data['trigger_conditions'] = trigger_conditions + + success = autonotes_manager.update_note(note_uuid, **data) + return web.json_response({"success": success}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.delete("/autonotes/notes/{note_uuid}") +async def delete_note(request): + try: + note_uuid = request.match_info['note_uuid'] + success = autonotes_manager.delete_note(note_uuid) + return web.json_response({"success": success}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.get("/autonotes/folders") +async def get_folders(request): + try: + folders_data = [asdict(folder) for folder in autonotes_manager.folders.values()] + return web.json_response(folders_data) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.post("/autonotes/folders") +async def create_folder(request): + try: + data = await request.json() + name = data.get('name', 'New Folder') + parent_uuid = data.get('parent_uuid') + + folder_uuid = autonotes_manager.create_folder(name, parent_uuid) + return web.json_response({"uuid": folder_uuid}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.put("/autonotes/folders/{folder_uuid}") +async def update_folder(request): + try: + folder_uuid = request.match_info['folder_uuid'] + data = await request.json() + + if folder_uuid not in autonotes_manager.folders: + return web.json_response({"success": False, "error": "Folder not found"}, status=404) + + folder = autonotes_manager.folders[folder_uuid] + if 'name' in data: + folder.name = data['name'] + if 'parent_uuid' in data: + folder.parent_uuid = data['parent_uuid'] + + autonotes_manager._save_folders() + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.delete("/autonotes/folders/{folder_uuid}") +async def delete_folder(request): + try: + folder_uuid = request.match_info['folder_uuid'] + + if folder_uuid not in autonotes_manager.folders: + return web.json_response({"success": False, "error": "Folder not found"}, status=404) + + # Remove folder_uuid from any notes that reference it + for note in autonotes_manager.notes.values(): + if note.folder_uuid == folder_uuid: + note.folder_uuid = None + autonotes_manager._save_notes() + + # Delete the folder + del autonotes_manager.folders[folder_uuid] + autonotes_manager._save_folders() + + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +# Node mappings +NODE_CLASS_MAPPINGS = { + "AutoNotesNode": AutoNotesNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AutoNotesNode": "Auto Notes" +} + +# Web directory for frontend files +WEB_DIRECTORY = "./web" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7b1206b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +requires = ["setuptools>=70.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ComfyUI-AutoNotes" +version = "0.0.1" +description = "Automatically display context-aware notes based on workflow state, selected nodes, and node attributes in ComfyUI." +authors = [ + {name = "John Cantu", email = "jfcantu@gmail.com"} +] +readme = "README.md" +license = {text = "MIT license"} +requires-python = ">=3.10" +classifiers = [] +dependencies = [] + +[project.urls] +Repository = "https://github.com/jfcantu/ComfyUI-AutoNotes" +BugTracker = "https://github.com/jfcantu/ComfyUI-AutoNotes/issues" +Documentation = "https://github.com/jfcantu/ComfyUI-AutoNotes/wiki" + + +[tool.comfy] +PublisherId = "jfcantu" +DisplayName = "ComfyUI AutoNotes" +Icon = "" +Tags = [] +Repository = "https://github.com/jfcantu/ComfyUI-AutoNotes" + +includes = [] + +[tool.setuptools.package-data] +"*" = ["*.*"] + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = [ + "tests", +] + +[tool.mypy] +files = "." + +# Use strict defaults +strict = true +warn_unreachable = true +warn_no_return = true + +[[tool.mypy.overrides]] +# Don't require test functions to include types +module = "tests.*" +allow_untyped_defs = true +disable_error_code = "attr-defined" + +[tool.ruff] +# extend-exclude = ["static", "ci/templates"] +line-length = 140 +src = ["src", "tests"] +target-version = "py39" + +# Add rules to ban exec/eval +[tool.ruff.lint] +select = [ + "S102", # exec-builtin + "S307", # eval-used + "W293", + "F", # The "F" series in Ruff stands for "Pyflakes" rules, which catch various Python syntax errors and undefined names. + # See all rules here: https://docs.astral.sh/ruff/rules/#pyflakes-f +] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" diff --git a/web/autonotes.js b/web/autonotes.js new file mode 100644 index 0000000..82a79ad --- /dev/null +++ b/web/autonotes.js @@ -0,0 +1,2983 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +class AutoNotesManager { + constructor() { + this.sidebar = null; + // Load last display mode from localStorage, default to "all" + this.displayMode = localStorage.getItem('autonotes_displayMode') || "all"; + this.notes = []; + this.folders = []; + this.editDialog = null; + this.selectedNodeType = null; + this.selectedNodeAttributes = {}; + this.workflowName = ""; + this.nodeTypes = {}; // Store node types and their definitions + this.expandedFolders = new Set(); // Track which folders are expanded + this.lastSelectedNodeType = null; // Track last selected node to detect changes + + this.init(); + } + + async init() { + // Wait for app to be ready + await new Promise(resolve => { + if (app.canvas) { + resolve(); + } else { + app.registerExtension({ + name: "AutoNotes.Init", + setup() { + resolve(); + } + }); + } + }); + + // Load marked library + await this.loadMarkedLibrary(); + + // Load node definitions + await this.loadNodeTypes(); + + this.createSidebar(); + this.setupEventListeners(); + await this.refreshNotes(); + } + + async loadMarkedLibrary() { + // Load the marked library using script tag since it's UMD format + if (typeof window.marked !== 'undefined') { + this.marked = window.marked; + return; + } + + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = '/extensions/ComfyUI-AutoNotes/marked.min.js'; + script.onload = () => { + this.marked = window.marked; + resolve(); + }; + script.onerror = () => { + console.error('Failed to load marked library'); + this.marked = null; + resolve(); // Resolve anyway, will use fallback + }; + document.head.appendChild(script); + }); + } + + async loadNodeTypes() { + try { + const response = await api.fetchApi('/object_info'); + this.nodeTypes = await response.json(); + } catch (error) { + console.error('Failed to load node types:', error); + this.nodeTypes = {}; + } + } + + createSidebar() { + // Load saved width from localStorage + const savedWidth = localStorage.getItem('autonotes_sidebarWidth') || '300'; + + // Calculate top bar height dynamically + const calculateTopBarHeight = () => { + // Check if user has manually set a value in localStorage + const manualHeight = localStorage.getItem('autonotes_topBarHeight'); + if (manualHeight) { + const height = parseInt(manualHeight); + console.log('AutoNotes: Using manually configured top bar height:', height); + return height; + } + + // Look for ComfyUI's main menu bar that contains buttons like Manager, Queue, etc. + const allElements = document.querySelectorAll('*'); + let candidates = []; + + for (const el of allElements) { + const style = window.getComputedStyle(el); + const position = style.position; + + if (position === 'fixed' || position === 'absolute') { + const rect = el.getBoundingClientRect(); + // Look for elements near the top that are reasonably wide (likely to be top bars) + // Relaxed criteria: width > 300 instead of 500 + if (rect.top >= 0 && rect.top <= 100 && rect.width > 300 && rect.bottom < 250 && rect.bottom > 50) { + candidates.push({ bottom: rect.bottom, width: rect.width, element: el.tagName }); + } + } + } + + // Sort by bottom position + if (candidates.length > 0) { + candidates.sort((a, b) => b.bottom - a.bottom); + const topBarBottom = Math.ceil(candidates[0].bottom); + console.log('AutoNotes: Detected top bar height:', topBarBottom, 'from', candidates[0].element, 'candidates:', candidates.length); + console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")'); + return topBarBottom; + } + + // Fallback - use a reasonable default that's close to the top bar + console.log('AutoNotes: Using fallback top bar height: 140 (no candidates found)'); + console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")'); + return 140; + }; + + const topBarHeight = calculateTopBarHeight(); + + // Create sidebar container - full height with padding at top + this.sidebar = document.createElement('div'); + this.sidebar.id = 'autonotes-sidebar'; + this.sidebar.style.cssText = ` + position: fixed; + right: 0; + top: 0; + width: ${savedWidth}px; + height: 100vh; + background: #2a2a2a; + border-left: 1px solid #555; + z-index: 1000; + display: flex; + flex-direction: column; + padding: ${topBarHeight}px 10px 10px 10px; + box-sizing: border-box; + font-family: Arial, sans-serif; + color: #fff; + `; + + // Create resize handle + const resizeHandle = document.createElement('div'); + resizeHandle.style.cssText = ` + position: absolute; + left: 0; + top: 0; + width: 5px; + height: 100%; + cursor: ew-resize; + background: transparent; + z-index: 1001; + `; + + resizeHandle.addEventListener('mouseenter', () => { + resizeHandle.style.background = '#007acc'; + }); + + resizeHandle.addEventListener('mouseleave', () => { + if (!this.isResizing) { + resizeHandle.style.background = 'transparent'; + } + }); + + // Resize functionality + this.isResizing = false; + let startX, startWidth; + + resizeHandle.addEventListener('mousedown', (e) => { + this.isResizing = true; + startX = e.clientX; + startWidth = parseInt(this.sidebar.style.width); + resizeHandle.style.background = '#007acc'; + document.body.style.cursor = 'ew-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', (e) => { + if (!this.isResizing) return; + + const dx = startX - e.clientX; + const newWidth = Math.max(200, Math.min(800, startWidth + dx)); + this.sidebar.style.width = newWidth + 'px'; + }); + + document.addEventListener('mouseup', () => { + if (this.isResizing) { + this.isResizing = false; + resizeHandle.style.background = 'transparent'; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Save width to localStorage + localStorage.setItem('autonotes_sidebarWidth', parseInt(this.sidebar.style.width)); + } + }); + + this.sidebar.appendChild(resizeHandle); + + // Control panel + const controlPanel = document.createElement('div'); + controlPanel.style.cssText = ` + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #555; + `; + + // Display mode selector + const modeContainer = document.createElement('div'); + modeContainer.style.marginBottom = '10px'; + + const modeLabel = document.createElement('label'); + modeLabel.textContent = 'Display: '; + modeLabel.style.marginRight = '10px'; + + const modeSelect = document.createElement('select'); + modeSelect.style.cssText = ` + background: #333; + color: #fff; + border: 1px solid #555; + padding: 5px; + `; + modeSelect.innerHTML = ` + + + `; + modeSelect.value = this.displayMode; + modeSelect.addEventListener('change', (e) => { + this.displayMode = e.target.value; + localStorage.setItem('autonotes_displayMode', this.displayMode); + this.refreshNotes(); + }); + + modeContainer.appendChild(modeLabel); + modeContainer.appendChild(modeSelect); + + // Buttons + const buttonContainer = document.createElement('div'); + buttonContainer.style.display = 'flex'; + buttonContainer.style.gap = '10px'; + + const addButton = document.createElement('button'); + addButton.textContent = 'Add'; + addButton.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + addButton.addEventListener('click', () => this.addNote()); + + const editButton = document.createElement('button'); + editButton.textContent = 'Edit'; + editButton.style.cssText = ` + background: #666; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + editButton.addEventListener('click', () => this.openEditDialog()); + + buttonContainer.appendChild(addButton); + buttonContainer.appendChild(editButton); + + controlPanel.appendChild(modeContainer); + controlPanel.appendChild(buttonContainer); + + // Notes display area + this.notesContainer = document.createElement('div'); + this.notesContainer.style.cssText = ` + flex: 1; + overflow-y: auto; + padding-right: 5px; + `; + + this.sidebar.appendChild(controlPanel); + this.sidebar.appendChild(this.notesContainer); + + document.body.appendChild(this.sidebar); + } + + setupEventListeners() { + // Use polling to check for selection changes (more reliable) + this.lastSelectionCheck = null; + setInterval(() => { + if (app.canvas && app.canvas.selected_nodes) { + const selectedNodeIds = Object.keys(app.canvas.selected_nodes); + const currentSelection = selectedNodeIds.join(','); + + // Only update if selection changed + if (currentSelection !== this.lastSelectionCheck) { + this.lastSelectionCheck = currentSelection; + this.updateSelectedNode(); + } + } + }, 300); // Check every 300ms + + // Listen for workflow changes + api.addEventListener("graphChanged", () => { + this.updateWorkflowName(); + }); + } + + updateSelectedNode() { + // Try multiple methods to get selected nodes + let selectedNodes = app.canvas?.selected_nodes; + + // Alternative: check graph canvas + if (!selectedNodes && app.graph?.list_of_graphcanvas?.[0]) { + selectedNodes = app.graph.list_of_graphcanvas[0].selected_nodes; + } + + let currentNodeType = null; + + // Check if it's an object with keys (not a Set) + const selectedNodeIds = selectedNodes ? Object.keys(selectedNodes) : []; + + if (selectedNodeIds.length > 0) { + // Get the first selected node + const nodeId = selectedNodeIds[0]; + const node = selectedNodes[nodeId]; + + if (node && node.type) { + currentNodeType = node.type; + this.selectedNodeType = node.type; + + // Collect node attributes (widgets/inputs) + this.selectedNodeAttributes = {}; + if (node.widgets) { + for (const widget of node.widgets) { + this.selectedNodeAttributes[widget.name] = widget.value; + } + } + } + } else { + this.selectedNodeType = null; + this.selectedNodeAttributes = {}; + } + + // Only refresh if selection changed and in automatic mode + if (this.displayMode === "automatic" && currentNodeType !== this.lastSelectedNodeType) { + this.lastSelectedNodeType = currentNodeType; + this.refreshNotes(); + } + } + + updateWorkflowName() { + // Try to get workflow name from various sources + this.workflowName = app.ui.lastQueueSize || document.title || ""; + + if (this.displayMode === "automatic") { + this.refreshNotes(); + } + } + + async refreshNotes() { + try { + const params = new URLSearchParams({ + mode: this.displayMode + }); + + if (this.selectedNodeType) { + params.append('node_type', this.selectedNodeType); + } + + if (Object.keys(this.selectedNodeAttributes).length > 0) { + params.append('node_attributes', JSON.stringify(this.selectedNodeAttributes)); + } + + if (this.workflowName) { + params.append('workflow_name', this.workflowName); + } + + // Collect workflow node information for node_in_workflow and attribute_in_workflow triggers + const workflowNodes = {}; + if (app.graph && app.graph._nodes) { + for (const node of app.graph._nodes) { + // For each node type in workflow, collect first instance's attributes + if (!workflowNodes[node.type]) { + const attributes = {}; + if (node.properties) { + Object.assign(attributes, node.properties); + } + if (node.widgets) { + for (const widget of node.widgets) { + attributes[widget.name] = widget.value; + } + } + workflowNodes[node.type] = attributes; + } + } + } + + if (Object.keys(workflowNodes).length > 0) { + params.append('workflow_nodes', JSON.stringify(workflowNodes)); + } + + const response = await api.fetchApi(`/autonotes/notes?${params}`); + this.notes = await response.json(); + this.renderNotes(); + } catch (error) { + console.error('Failed to fetch notes:', error); + } + } + + renderNotes() { + this.notesContainer.innerHTML = ''; + + if (this.notes.length === 0) { + const emptyMessage = document.createElement('div'); + emptyMessage.textContent = 'No notes to display'; + emptyMessage.style.cssText = ` + text-align: center; + color: #888; + font-style: italic; + margin-top: 20px; + `; + this.notesContainer.appendChild(emptyMessage); + return; + } + + for (const note of this.notes) { + this.renderNote(note); + } + } + + renderNote(note) { + const noteElement = document.createElement('div'); + noteElement.style.cssText = ` + margin-bottom: 15px; + border: 1px solid #555; + border-radius: 4px; + overflow: hidden; + `; + + // Note header (collapsible) + const header = document.createElement('div'); + header.style.cssText = ` + background: #333; + padding: 10px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + `; + + const title = document.createElement('span'); + title.textContent = note.name || 'Untitled Note'; + title.style.fontWeight = 'bold'; + + const controls = document.createElement('div'); + controls.style.display = 'flex'; + controls.style.gap = '5px'; + controls.style.alignItems = 'center'; + + // Pin button + const pinButton = document.createElement('button'); + pinButton.textContent = note.pinned ? '📌' : '📍'; + pinButton.title = note.pinned ? 'Unpin' : 'Pin'; + pinButton.style.cssText = ` + background: transparent; + border: none; + color: #fff; + cursor: pointer; + padding: 0; + font-size: 16px; + opacity: ${note.pinned ? '1' : '0.5'}; + `; + pinButton.addEventListener('click', async (e) => { + e.stopPropagation(); + await this.togglePin(note); + }); + controls.appendChild(pinButton); + + // Collapse indicator + const collapseIcon = document.createElement('span'); + collapseIcon.textContent = '▼'; + collapseIcon.style.transition = 'transform 0.2s'; + controls.appendChild(collapseIcon); + + header.appendChild(title); + header.appendChild(controls); + + // Content container + const contentContainer = document.createElement('div'); + contentContainer.style.cssText = ` + background: #2a2a2a; + border-top: 1px solid #555; + `; + + // Mode toggle (for markdown notes) + let isEditMode = false; + let modeToggle = null; + + if (note.format_style === 'markdown') { + const toggleBar = document.createElement('div'); + toggleBar.style.cssText = ` + padding: 5px 10px; + background: #252525; + border-bottom: 1px solid #555; + display: flex; + gap: 5px; + `; + + const viewBtn = document.createElement('button'); + viewBtn.textContent = 'View'; + viewBtn.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 3px 10px; + cursor: pointer; + border-radius: 3px; + font-size: 11px; + `; + + const editBtn = document.createElement('button'); + editBtn.textContent = 'Edit'; + editBtn.style.cssText = ` + background: #555; + color: white; + border: none; + padding: 3px 10px; + cursor: pointer; + border-radius: 3px; + font-size: 11px; + `; + + toggleBar.appendChild(viewBtn); + toggleBar.appendChild(editBtn); + contentContainer.appendChild(toggleBar); + + modeToggle = { viewBtn, editBtn }; + } + + // View content (rendered markdown or plain text) + const viewContent = document.createElement('div'); + viewContent.style.cssText = ` + padding: 10px; + `; + + if (note.format_style === 'markdown') { + viewContent.innerHTML = this.renderMarkdown(note.content); + } else { + viewContent.textContent = note.content; + viewContent.style.whiteSpace = 'pre-wrap'; + } + + // Edit content (textarea) + const editContent = document.createElement('textarea'); + editContent.value = note.content; + editContent.style.cssText = ` + width: 100%; + min-height: 100px; + padding: 10px; + background: #1a1a1a; + color: #fff; + border: none; + font-family: monospace; + font-size: 12px; + resize: vertical; + box-sizing: border-box; + display: none; + `; + + // Save button + const saveBtn = document.createElement('button'); + saveBtn.textContent = 'Save'; + saveBtn.style.cssText = ` + margin: 5px 10px 10px 10px; + background: #007acc; + color: white; + border: none; + padding: 5px 15px; + cursor: pointer; + border-radius: 3px; + display: none; + `; + + saveBtn.addEventListener('click', async () => { + await this.updateNoteContent(note.uuid, editContent.value); + }); + + contentContainer.appendChild(viewContent); + contentContainer.appendChild(editContent); + contentContainer.appendChild(saveBtn); + + // Toggle between view and edit mode + if (modeToggle) { + const switchToView = () => { + isEditMode = false; + viewContent.style.display = 'block'; + editContent.style.display = 'none'; + saveBtn.style.display = 'none'; + modeToggle.viewBtn.style.background = '#007acc'; + modeToggle.editBtn.style.background = '#555'; + }; + + const switchToEdit = () => { + isEditMode = true; + viewContent.style.display = 'none'; + editContent.style.display = 'block'; + saveBtn.style.display = 'block'; + modeToggle.viewBtn.style.background = '#555'; + modeToggle.editBtn.style.background = '#007acc'; + }; + + modeToggle.viewBtn.addEventListener('click', (e) => { + e.stopPropagation(); + switchToView(); + }); + + modeToggle.editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + switchToEdit(); + }); + } else { + // For plain text, click to edit directly + viewContent.addEventListener('click', (e) => { + e.stopPropagation(); + viewContent.style.display = 'none'; + editContent.style.display = 'block'; + saveBtn.style.display = 'block'; + editContent.focus(); + }); + } + + // Collapsible functionality + let isCollapsed = false; + header.addEventListener('click', () => { + isCollapsed = !isCollapsed; + contentContainer.style.display = isCollapsed ? 'none' : 'block'; + collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; + }); + + noteElement.appendChild(header); + noteElement.appendChild(contentContainer); + this.notesContainer.appendChild(noteElement); + } + + renderMarkdown(text) { + if (!text) return ''; + + // Use the bundled marked library if available + if (this.marked) { + try { + return this.marked.parse(text, { + breaks: true, + gfm: true + }); + } catch (error) { + console.error('Markdown rendering error:', error); + } + } + + // Fallback to plain text with line breaks + return text.replace(/\n/g, '
'); + } + + async addNote() { + // Load folders first if not already loaded + await this.loadFolders(); + + // Show custom dialog to get note name and folder + const result = await this.showAddNoteDialog(); + if (!result) return; + + try { + const response = await api.fetchApi('/autonotes/notes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: result.name, + folder_uuid: result.folder_uuid + }), + }); + + const noteResult = await response.json(); + if (noteResult.uuid) { + await this.refreshNotes(); + await this.openEditDialog(noteResult.uuid); + } + } catch (error) { + console.error('Failed to create note:', error); + alert('Failed to create note'); + } + } + + async showAddNoteDialog() { + return new Promise((resolve) => { + // Create overlay + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: 3000; + display: flex; + justify-content: center; + align-items: center; + `; + + // Create dialog + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #2a2a2a; + border: 1px solid #555; + border-radius: 8px; + padding: 20px; + width: 400px; + color: #fff; + font-family: Arial, sans-serif; + `; + + // Title + const title = document.createElement('h2'); + title.textContent = 'Create New Note'; + title.style.cssText = ` + margin: 0 0 20px 0; + font-size: 1.3em; + `; + + // Name input + const nameLabel = document.createElement('label'); + nameLabel.textContent = 'Note Name:'; + nameLabel.style.cssText = ` + display: block; + margin-bottom: 5px; + `; + + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.placeholder = 'Enter note name...'; + nameInput.style.cssText = ` + width: 100%; + padding: 8px; + background: #1a1a1a; + color: #fff; + border: 1px solid #555; + border-radius: 4px; + box-sizing: border-box; + margin-bottom: 20px; + font-size: 14px; + `; + + // Folder selection + const folderLabel = document.createElement('label'); + folderLabel.textContent = 'Folder:'; + folderLabel.style.cssText = ` + display: block; + margin-bottom: 5px; + `; + + const folderTreeContainer = document.createElement('div'); + folderTreeContainer.style.cssText = ` + width: 100%; + max-height: 200px; + overflow-y: auto; + background: #1a1a1a; + border: 1px solid #555; + border-radius: 4px; + margin-bottom: 20px; + padding: 8px; + `; + + let selectedFolderUuid = null; + + // Root option + const rootItem = document.createElement('div'); + rootItem.style.cssText = ` + padding: 5px 8px; + cursor: pointer; + border-radius: 3px; + background: #007acc; + margin-bottom: 5px; + `; + rootItem.textContent = '📁 [Root]'; + rootItem.dataset.folderUuid = ''; + + rootItem.addEventListener('click', () => { + // Remove selection from all items + folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => { + item.style.background = ''; + }); + rootItem.style.background = '#007acc'; + selectedFolderUuid = null; + }); + + folderTreeContainer.appendChild(rootItem); + + // Build folder tree recursively + // level 0 = [Root] + // level 1 = folders with parent_uuid = null (direct children of root) + // level 2+ = nested folders + const buildFolderTree = (parentUuid, level = 1) => { + // Filter folders by parent - handle both null and undefined + const childFolders = this.folders.filter(f => { + const folderParent = f.parent_uuid || null; + const searchParent = parentUuid || null; + return folderParent === searchParent; + }); + + for (const folder of childFolders) { + const folderItem = document.createElement('div'); + const indent = 8 + (level * 20); + folderItem.style.cssText = ` + padding: 5px 8px; + padding-left: ${indent}px; + cursor: pointer; + border-radius: 3px; + margin-bottom: 2px; + `; + folderItem.textContent = `${' '.repeat(level)}📁 ${folder.name}`; + folderItem.dataset.folderUuid = folder.uuid; + + folderItem.addEventListener('click', () => { + // Remove selection from all items + folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => { + item.style.background = ''; + }); + folderItem.style.background = '#007acc'; + selectedFolderUuid = folder.uuid; + }); + + folderTreeContainer.appendChild(folderItem); + + // Recursively build child folders + buildFolderTree(folder.uuid, level + 1); + } + }; + + // Start building from root (null parent) at level 1 + // Level 0 is [Root] itself, so children start at level 1 + buildFolderTree(null, 1); + + // Buttons + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + display: flex; + justify-content: flex-end; + gap: 10px; + `; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.style.cssText = ` + background: #666; + color: white; + border: none; + padding: 8px 20px; + cursor: pointer; + border-radius: 4px; + `; + + const createBtn = document.createElement('button'); + createBtn.textContent = 'Create'; + createBtn.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 8px 20px; + cursor: pointer; + border-radius: 4px; + `; + + // Event handlers + const closeDialog = (result) => { + overlay.remove(); + resolve(result); + }; + + cancelBtn.addEventListener('click', () => closeDialog(null)); + + createBtn.addEventListener('click', () => { + const name = nameInput.value.trim(); + if (!name) { + alert('Please enter a note name'); + return; + } + + closeDialog({ + name: name, + folder_uuid: selectedFolderUuid + }); + }); + + // Allow Enter key to create + nameInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + createBtn.click(); + } + }); + + // Focus name input + setTimeout(() => nameInput.focus(), 0); + + // Assemble dialog + buttonContainer.appendChild(cancelBtn); + buttonContainer.appendChild(createBtn); + + dialog.appendChild(title); + dialog.appendChild(nameLabel); + dialog.appendChild(nameInput); + dialog.appendChild(folderLabel); + dialog.appendChild(folderTreeContainer); + dialog.appendChild(buttonContainer); + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + closeDialog(null); + } + }); + }); + } + + async openEditDialog(noteUuid = null) { + if (this.editDialog) { + this.editDialog.remove(); + } + + await this.loadFolders(); + // Load all notes for the edit dialog, regardless of display mode + await this.loadAllNotes(); + this.createEditDialog(noteUuid); + } + + async loadAllNotes() { + try { + const response = await api.fetchApi('/autonotes/notes?mode=all'); + this.notes = await response.json(); + } catch (error) { + console.error('Failed to load all notes:', error); + } + } + + async loadFolders() { + try { + const response = await api.fetchApi('/autonotes/folders'); + this.folders = await response.json(); + } catch (error) { + console.error('Failed to load folders:', error); + this.folders = []; + } + } + + createEditDialog(selectedNoteUuid = null) { + // Create modal overlay + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: 2000; + display: flex; + justify-content: center; + align-items: center; + `; + + // Load saved dialog size + const savedWidth = localStorage.getItem('autonotes_editDialogWidth') || '800px'; + const savedHeight = localStorage.getItem('autonotes_editDialogHeight') || '600px'; + + // Create dialog + this.editDialog = document.createElement('div'); + this.editDialog.style.cssText = ` + background: #2a2a2a; + border: 1px solid #555; + border-radius: 8px; + width: ${savedWidth}; + height: ${savedHeight}; + display: flex; + color: #fff; + font-family: Arial, sans-serif; + position: relative; + `; + + // Track if we're currently resizing to prevent closing on overlay click + this.isResizingDialog = false; + + // Create resize handles + const createResizeHandle = (position, cursor) => { + const handle = document.createElement('div'); + handle.style.cssText = ` + position: absolute; + background: transparent; + z-index: 10; + `; + + if (position === 'right') { + handle.style.cssText += ` + right: 0; + top: 0; + width: 5px; + height: 100%; + cursor: ${cursor}; + `; + } else if (position === 'bottom') { + handle.style.cssText += ` + bottom: 0; + left: 0; + width: 100%; + height: 5px; + cursor: ${cursor}; + `; + } else if (position === 'corner') { + handle.style.cssText += ` + right: 0; + bottom: 0; + width: 15px; + height: 15px; + cursor: ${cursor}; + `; + } + + let isResizing = false; + let startX, startY, startWidth, startHeight; + + handle.addEventListener('mousedown', (e) => { + isResizing = true; + this.isResizingDialog = true; + startX = e.clientX; + startY = e.clientY; + startWidth = this.editDialog.offsetWidth; + startHeight = this.editDialog.offsetHeight; + document.body.style.userSelect = 'none'; + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + if (position === 'right' || position === 'corner') { + const newWidth = startWidth + (e.clientX - startX); + this.editDialog.style.width = Math.max(400, Math.min(window.innerWidth - 40, newWidth)) + 'px'; + } + + if (position === 'bottom' || position === 'corner') { + const newHeight = startHeight + (e.clientY - startY); + this.editDialog.style.height = Math.max(300, Math.min(window.innerHeight - 40, newHeight)) + 'px'; + } + }); + + document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + document.body.style.userSelect = ''; + + // Save size to localStorage + localStorage.setItem('autonotes_editDialogWidth', this.editDialog.style.width); + localStorage.setItem('autonotes_editDialogHeight', this.editDialog.style.height); + + // Delay clearing the flag to prevent overlay click from closing + setTimeout(() => { + this.isResizingDialog = false; + }, 100); + } + }); + + return handle; + }; + + this.editDialog.appendChild(createResizeHandle('right', 'ew-resize')); + this.editDialog.appendChild(createResizeHandle('bottom', 'ns-resize')); + this.editDialog.appendChild(createResizeHandle('corner', 'nwse-resize')); + + // Tree view (left side) + const treeContainer = document.createElement('div'); + treeContainer.style.cssText = ` + width: 250px; + border-right: 1px solid #555; + padding: 10px; + overflow-y: auto; + `; + + const treeHeader = document.createElement('div'); + treeHeader.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + `; + + const treeTitle = document.createElement('h3'); + treeTitle.textContent = 'Notes'; + treeTitle.style.margin = '0'; + + const folderButtons = document.createElement('div'); + folderButtons.style.display = 'flex'; + folderButtons.style.gap = '5px'; + + const addFolderButton = document.createElement('button'); + addFolderButton.textContent = '+'; + addFolderButton.title = 'Add Folder'; + addFolderButton.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 2px 8px; + cursor: pointer; + border-radius: 3px; + font-size: 16px; + `; + addFolderButton.addEventListener('click', () => this.addFolder(treeContainer, selectedNoteUuid)); + + folderButtons.appendChild(addFolderButton); + + treeHeader.appendChild(treeTitle); + treeHeader.appendChild(folderButtons); + treeContainer.appendChild(treeHeader); + + this.renderNoteTree(treeContainer, selectedNoteUuid); + + // Make tree container a drop target for root + treeContainer.addEventListener('dragover', (e) => { + // Check if we're over empty space (not over a folder or note) + const target = e.target; + if (target === treeContainer || target === treeHeader || target === treeTitle) { + e.preventDefault(); + treeContainer.style.background = 'rgba(0, 122, 204, 0.1)'; + } + }); + + treeContainer.addEventListener('dragleave', (e) => { + if (e.target === treeContainer) { + treeContainer.style.background = ''; + } + }); + + treeContainer.addEventListener('drop', async (e) => { + // Only handle if dropped on empty space + const target = e.target; + if (target === treeContainer || target === treeHeader || target === treeTitle) { + e.preventDefault(); + treeContainer.style.background = ''; + + const noteUuid = e.dataTransfer.getData('text/plain'); + if (noteUuid) { + await this.moveNoteToFolder(noteUuid, null, treeContainer, selectedNoteUuid); + } + } + }); + + // Edit panel (right side) + const editPanel = document.createElement('div'); + editPanel.style.cssText = ` + flex: 1; + padding: 10px; + display: flex; + flex-direction: column; + `; + + this.createEditPanel(editPanel, selectedNoteUuid); + + this.editDialog.appendChild(treeContainer); + this.editDialog.appendChild(editPanel); + + overlay.appendChild(this.editDialog); + document.body.appendChild(overlay); + } + + renderNoteTree(container, selectedNoteUuid) { + // Clear existing tree items (but not the header) + const existingItems = container.querySelectorAll('.tree-item, .folder-item'); + existingItems.forEach(item => item.remove()); + + // Render folders first + for (const folder of this.folders) { + const folderItem = this.createFolderItem(folder, container, selectedNoteUuid); + container.appendChild(folderItem); + } + + // Render notes (only top-level notes without folders for now) + for (const note of this.notes) { + if (!note.folder_uuid) { + const noteItem = this.createNoteItem(note, selectedNoteUuid, container); + container.appendChild(noteItem); + } + } + } + + createFolderItem(folder, treeContainer, selectedNoteUuid) { + const folderItem = document.createElement('div'); + folderItem.className = 'folder-item'; + folderItem.style.cssText = ` + margin-bottom: 5px; + `; + + const folderHeader = document.createElement('div'); + folderHeader.style.cssText = ` + display: flex; + align-items: center; + gap: 5px; + padding: 5px; + border-radius: 3px; + background: #333; + cursor: pointer; + `; + + // Collapse/expand icon + const expandIcon = document.createElement('span'); + expandIcon.textContent = '▶'; + expandIcon.style.cssText = ` + transition: transform 0.2s; + font-size: 10px; + `; + + // Make folder a drop target + folderHeader.addEventListener('dragover', (e) => { + e.preventDefault(); + folderHeader.style.background = '#007acc'; + }); + + folderHeader.addEventListener('dragleave', (e) => { + folderHeader.style.background = '#333'; + }); + + folderHeader.addEventListener('drop', async (e) => { + e.preventDefault(); + folderHeader.style.background = '#333'; + + const noteUuid = e.dataTransfer.getData('text/plain'); + if (noteUuid) { + await this.moveNoteToFolder(noteUuid, folder.uuid, treeContainer, selectedNoteUuid); + } + }); + + const folderName = document.createElement('span'); + folderName.textContent = `📁 ${folder.name}`; + folderName.style.flex = '1'; + + const renameBtn = document.createElement('button'); + renameBtn.textContent = '✏️'; + renameBtn.title = 'Rename Folder'; + renameBtn.style.cssText = ` + background: transparent; + border: none; + color: #fff; + cursor: pointer; + padding: 2px; + font-size: 12px; + `; + renameBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.renameFolder(folder, treeContainer, selectedNoteUuid); + }); + + const deleteBtn = document.createElement('button'); + deleteBtn.textContent = '🗑️'; + deleteBtn.title = 'Delete Folder'; + deleteBtn.style.cssText = ` + background: transparent; + border: none; + color: #fff; + cursor: pointer; + padding: 2px; + font-size: 12px; + `; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.deleteFolder(folder, treeContainer, selectedNoteUuid); + }); + + folderHeader.appendChild(expandIcon); + folderHeader.appendChild(folderName); + folderHeader.appendChild(renameBtn); + folderHeader.appendChild(deleteBtn); + + // Notes container (children) + const notesContainer = document.createElement('div'); + notesContainer.style.cssText = ` + padding-left: 20px; + display: none; + `; + + // Find notes in this folder + const folderNotes = this.notes.filter(note => note.folder_uuid === folder.uuid); + for (const note of folderNotes) { + const noteItem = this.createNoteItem(note, selectedNoteUuid, treeContainer); + notesContainer.appendChild(noteItem); + } + + // Toggle expand/collapse + const isExpanded = this.expandedFolders.has(folder.uuid); + notesContainer.style.display = isExpanded ? 'block' : 'none'; + expandIcon.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'; + + folderHeader.addEventListener('click', (e) => { + // Don't toggle if clicking buttons + if (e.target === renameBtn || e.target === deleteBtn) { + return; + } + + if (this.expandedFolders.has(folder.uuid)) { + this.expandedFolders.delete(folder.uuid); + notesContainer.style.display = 'none'; + expandIcon.style.transform = 'rotate(0deg)'; + } else { + this.expandedFolders.add(folder.uuid); + notesContainer.style.display = 'block'; + expandIcon.style.transform = 'rotate(90deg)'; + } + }); + + folderItem.appendChild(folderHeader); + folderItem.appendChild(notesContainer); + + return folderItem; + } + + createNoteItem(note, selectedNoteUuid, treeContainer) { + const noteItem = document.createElement('div'); + noteItem.className = 'tree-item'; + noteItem.draggable = true; + noteItem.style.cssText = ` + padding: 5px; + cursor: move; + border-radius: 3px; + margin-bottom: 2px; + ${note.uuid === selectedNoteUuid ? 'background: #007acc;' : ''} + `; + noteItem.textContent = note.name || 'Untitled'; + + // Drag start + noteItem.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', note.uuid); + noteItem.style.opacity = '0.5'; + }); + + // Drag end + noteItem.addEventListener('dragend', (e) => { + noteItem.style.opacity = '1'; + }); + + // Left click to select + noteItem.addEventListener('click', () => { + // Remove previous selection + treeContainer.querySelectorAll('.tree-item').forEach(item => { + item.style.background = ''; + }); + noteItem.style.background = '#007acc'; + + this.loadNoteIntoEditor(note.uuid); + }); + + // Right click for context menu + noteItem.addEventListener('contextmenu', (e) => { + e.preventDefault(); + this.showNoteContextMenu(e, note, treeContainer, selectedNoteUuid); + }); + + return noteItem; + } + + createEditPanel(panel, selectedNoteUuid) { + // Content textarea + const contentContainer = document.createElement('div'); + contentContainer.style.cssText = ` + flex: 1; + display: flex; + flex-direction: column; + margin-bottom: 10px; + `; + + const contentLabel = document.createElement('label'); + contentLabel.textContent = 'Content: '; + contentLabel.style.display = 'block'; + contentLabel.style.marginBottom = '5px'; + + this.contentTextarea = document.createElement('textarea'); + this.contentTextarea.style.cssText = ` + flex: 1; + padding: 10px; + background: #333; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + resize: none; + font-family: monospace; + `; + + contentContainer.appendChild(contentLabel); + contentContainer.appendChild(this.contentTextarea); + + // Format style + const formatContainer = document.createElement('div'); + formatContainer.style.cssText = ` + display: flex; + gap: 20px; + margin-bottom: 10px; + align-items: center; + `; + + const formatDiv = document.createElement('div'); + const formatLabel = document.createElement('label'); + formatLabel.textContent = 'Format: '; + formatLabel.style.marginRight = '10px'; + + this.formatSelect = document.createElement('select'); + this.formatSelect.style.cssText = ` + background: #333; + color: #fff; + border: 1px solid #555; + padding: 5px; + `; + this.formatSelect.innerHTML = ` + + + `; + + formatDiv.appendChild(formatLabel); + formatDiv.appendChild(this.formatSelect); + + // Pinned checkbox + const pinnedDiv = document.createElement('div'); + this.pinnedCheckbox = document.createElement('input'); + this.pinnedCheckbox.type = 'checkbox'; + this.pinnedCheckbox.id = 'note-pinned'; + + const pinnedLabel = document.createElement('label'); + pinnedLabel.htmlFor = 'note-pinned'; + pinnedLabel.textContent = 'Pinned'; + pinnedLabel.style.marginLeft = '5px'; + + pinnedDiv.appendChild(this.pinnedCheckbox); + pinnedDiv.appendChild(pinnedLabel); + + formatContainer.appendChild(formatDiv); + formatContainer.appendChild(pinnedDiv); + + // Trigger conditions section + const triggerContainer = document.createElement('div'); + triggerContainer.style.cssText = ` + margin-bottom: 10px; + border: 1px solid #555; + border-radius: 4px; + padding: 10px; + max-height: 200px; + overflow-y: auto; + `; + + const triggerTitle = document.createElement('h4'); + triggerTitle.textContent = 'Trigger Conditions'; + triggerTitle.style.margin = '0 0 10px 0'; + + const addTriggerButton = document.createElement('button'); + addTriggerButton.textContent = 'Add Trigger'; + addTriggerButton.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 5px 10px; + cursor: pointer; + border-radius: 3px; + margin-bottom: 10px; + `; + addTriggerButton.addEventListener('click', () => this.addTriggerCondition()); + + this.triggerList = document.createElement('div'); + + triggerContainer.appendChild(triggerTitle); + triggerContainer.appendChild(addTriggerButton); + triggerContainer.appendChild(this.triggerList); + + // Buttons + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + display: flex; + justify-content: space-between; + margin-top: 10px; + `; + + const leftButtons = document.createElement('div'); + leftButtons.style.display = 'flex'; + leftButtons.style.gap = '10px'; + + const saveButton = document.createElement('button'); + saveButton.textContent = 'Save'; + saveButton.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + saveButton.addEventListener('click', () => this.saveCurrentNote()); + + const revertButton = document.createElement('button'); + revertButton.textContent = 'Revert'; + revertButton.style.cssText = ` + background: #666; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + + leftButtons.appendChild(saveButton); + leftButtons.appendChild(revertButton); + + const rightButtons = document.createElement('div'); + rightButtons.style.display = 'flex'; + rightButtons.style.gap = '10px'; + + const okButton = document.createElement('button'); + okButton.textContent = 'OK'; + okButton.style.cssText = ` + background: #007acc; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + okButton.addEventListener('click', () => this.closeEditDialog(true)); + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Cancel'; + cancelButton.style.cssText = ` + background: #666; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + `; + cancelButton.addEventListener('click', () => this.closeEditDialog(false)); + + rightButtons.appendChild(okButton); + rightButtons.appendChild(cancelButton); + + buttonContainer.appendChild(leftButtons); + buttonContainer.appendChild(rightButtons); + + panel.appendChild(contentContainer); + panel.appendChild(formatContainer); + panel.appendChild(triggerContainer); + panel.appendChild(buttonContainer); + + // Load initial note if specified + if (selectedNoteUuid) { + this.loadNoteIntoEditor(selectedNoteUuid); + } + } + + loadNoteIntoEditor(noteUuid) { + const note = this.notes.find(n => n.uuid === noteUuid); + if (!note) return; + + this.currentEditingNote = note; + this.contentTextarea.value = note.content || ''; + this.formatSelect.value = note.format_style || 'plaintext'; + this.pinnedCheckbox.checked = note.pinned || false; + + // Load trigger conditions + this.currentTriggerConditions = [...(note.trigger_conditions || [])]; + this.renderTriggerConditions(); + } + + addTriggerCondition() { + const condition = { + type: 'node_selected', + node_types: [], + node_type: null, + attribute_name: null, + attribute_values: [], + workflow_names: [] + }; + + this.currentTriggerConditions = this.currentTriggerConditions || []; + this.currentTriggerConditions.push(condition); + this.renderTriggerConditions(); + } + + renderTriggerConditions() { + this.triggerList.innerHTML = ''; + + if (!this.currentTriggerConditions) { + this.currentTriggerConditions = []; + } + + this.currentTriggerConditions.forEach((condition, index) => { + const conditionElement = this.createTriggerConditionElement(condition, index); + this.triggerList.appendChild(conditionElement); + }); + } + + createTriggerConditionElement(condition, index) { + const container = document.createElement('div'); + container.style.cssText = ` + border: 1px solid #666; + border-radius: 3px; + margin-bottom: 10px; + background: #333; + `; + + // Header (collapsible) + const header = document.createElement('div'); + header.style.cssText = ` + display: flex; + align-items: center; + padding: 10px; + cursor: pointer; + background: #3a3a3a; + border-radius: 3px 3px 0 0; + `; + + // Collapse icon + const collapseIcon = document.createElement('span'); + collapseIcon.textContent = '▼'; + collapseIcon.style.cssText = ` + transition: transform 0.2s; + margin-right: 10px; + font-size: 10px; + `; + + // Title showing condition type + const titleSpan = document.createElement('span'); + titleSpan.style.flex = '1'; + const getConditionTitle = () => { + if (condition.type === 'node_selected') return 'Node Selected'; + if (condition.type === 'node_attribute') return 'Node Attribute'; + if (condition.type === 'node_in_workflow') return 'Node in Workflow'; + if (condition.type === 'attribute_in_workflow') return 'Attribute in Workflow'; + if (condition.type === 'workflow_name') return 'Workflow Name'; + return 'Trigger Condition'; + }; + titleSpan.textContent = getConditionTitle(); + + header.appendChild(collapseIcon); + header.appendChild(titleSpan); + + // Content container (collapsible) + const content = document.createElement('div'); + content.style.cssText = ` + padding: 10px; + display: block; + `; + + // Type selector + const typeContainer = document.createElement('div'); + typeContainer.style.cssText = ` + display: flex; + align-items: center; + margin-bottom: 10px; + `; + + const typeLabel = document.createElement('label'); + typeLabel.textContent = 'Type: '; + typeLabel.style.marginRight = '10px'; + + const typeSelect = document.createElement('select'); + typeSelect.style.cssText = ` + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + padding: 5px; + margin-right: 10px; + `; + typeSelect.innerHTML = ` + + + + + + `; + typeSelect.value = condition.type; + + const removeButton = document.createElement('button'); + removeButton.textContent = 'Remove'; + removeButton.style.cssText = ` + background: #cc0000; + color: white; + border: none; + padding: 5px 10px; + cursor: pointer; + border-radius: 3px; + margin-left: auto; + `; + removeButton.addEventListener('click', (e) => { + e.stopPropagation(); + this.currentTriggerConditions.splice(index, 1); + this.renderTriggerConditions(); + }); + + typeContainer.appendChild(typeLabel); + typeContainer.appendChild(typeSelect); + typeContainer.appendChild(removeButton); + + // Fields container for type-specific fields + const fieldsContainer = document.createElement('div'); + + const updateContent = () => { + condition.type = typeSelect.value; + titleSpan.textContent = getConditionTitle(); + fieldsContainer.innerHTML = ''; + + if (condition.type === 'node_selected') { + this.createNodeSelectedFields(fieldsContainer, condition); + } else if (condition.type === 'node_attribute') { + this.createNodeAttributeFields(fieldsContainer, condition); + } else if (condition.type === 'node_in_workflow') { + this.createNodeInWorkflowFields(fieldsContainer, condition); + } else if (condition.type === 'attribute_in_workflow') { + this.createAttributeInWorkflowFields(fieldsContainer, condition); + } else if (condition.type === 'workflow_name') { + this.createWorkflowNameFields(fieldsContainer, condition); + } + }; + + typeSelect.addEventListener('change', updateContent); + updateContent(); + + content.appendChild(typeContainer); + content.appendChild(fieldsContainer); + + // Collapse/expand functionality + let isCollapsed = false; + header.addEventListener('click', (e) => { + // Don't collapse if clicking on buttons/inputs + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') { + return; + } + + isCollapsed = !isCollapsed; + content.style.display = isCollapsed ? 'none' : 'block'; + collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; + }); + + container.appendChild(header); + container.appendChild(content); + + return container; + } + + createNodeSelectedFields(container, condition) { + const label = document.createElement('label'); + label.textContent = 'Node Types: '; + label.style.display = 'block'; + label.style.marginBottom = '5px'; + + // Multi-select dropdown with searchable autocomplete + const selectContainer = document.createElement('div'); + selectContainer.style.cssText = ` + position: relative; + width: 100%; + `; + + // Create a searchable input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search node types...'; + searchInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + margin-bottom: 5px; + `; + + // Selected items display + const selectedDisplay = document.createElement('div'); + selectedDisplay.style.cssText = ` + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-bottom: 10px; + min-height: 30px; + padding: 5px; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + `; + + // Dropdown list + const dropdownList = document.createElement('div'); + dropdownList.style.cssText = ` + position: absolute; + width: 100%; + max-height: 200px; + overflow-y: auto; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + z-index: 1000; + display: none; + `; + + // Get node types from workflow + const workflowNodeTypes = new Set(); + if (app.graph && app.graph._nodes) { + for (const node of app.graph._nodes) { + workflowNodeTypes.add(node.type); + } + } + + // Populate dropdown with all node types + const nodeTypesList = Object.keys(this.nodeTypes).sort(); + + const updateSelectedDisplay = () => { + selectedDisplay.innerHTML = ''; + (condition.node_types || []).forEach(nodeType => { + const tag = document.createElement('span'); + tag.style.cssText = ` + background: #007acc; + color: #fff; + padding: 3px 8px; + border-radius: 3px; + display: inline-flex; + align-items: center; + gap: 5px; + `; + + // Add icon if in workflow + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + tag.appendChild(icon); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; + tag.appendChild(nameSpan); + + const removeBtn = document.createElement('span'); + removeBtn.textContent = '×'; + removeBtn.style.cursor = 'pointer'; + removeBtn.style.fontWeight = 'bold'; + removeBtn.addEventListener('click', () => { + condition.node_types = condition.node_types.filter(t => t !== nodeType); + updateSelectedDisplay(); + }); + tag.appendChild(removeBtn); + + selectedDisplay.appendChild(tag); + }); + }; + + const filterDropdown = (searchTerm) => { + dropdownList.innerHTML = ''; + const filtered = nodeTypesList.filter(nodeType => { + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + return displayName.toLowerCase().includes(searchTerm.toLowerCase()) || + nodeType.toLowerCase().includes(searchTerm.toLowerCase()); + }); + + filtered.forEach(nodeType => { + const option = document.createElement('div'); + option.style.cssText = ` + padding: 8px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + `; + + // Add checkmark if in workflow + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + icon.style.fontWeight = 'bold'; + option.appendChild(icon); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; + option.appendChild(nameSpan); + + option.addEventListener('mouseenter', () => { + option.style.background = '#007acc'; + }); + option.addEventListener('mouseleave', () => { + option.style.background = ''; + }); + option.addEventListener('click', () => { + if (!condition.node_types) condition.node_types = []; + if (!condition.node_types.includes(nodeType)) { + condition.node_types.push(nodeType); + updateSelectedDisplay(); + } + searchInput.value = ''; + dropdownList.style.display = 'none'; + }); + + dropdownList.appendChild(option); + }); + + dropdownList.style.display = filtered.length > 0 ? 'block' : 'none'; + }; + + searchInput.addEventListener('focus', () => { + filterDropdown(searchInput.value); + }); + + searchInput.addEventListener('input', () => { + filterDropdown(searchInput.value); + }); + + // Close dropdown when clicking outside + document.addEventListener('click', (e) => { + if (!selectContainer.contains(e.target)) { + dropdownList.style.display = 'none'; + } + }); + + selectContainer.appendChild(searchInput); + selectContainer.appendChild(dropdownList); + + container.appendChild(label); + container.appendChild(selectedDisplay); + container.appendChild(selectContainer); + + // Initialize display + updateSelectedDisplay(); + } + + createNodeAttributeFields(container, condition) { + // Node type searchable dropdown + const nodeTypeContainer = document.createElement('div'); + nodeTypeContainer.style.marginBottom = '10px'; + + const nodeTypeLabel = document.createElement('label'); + nodeTypeLabel.textContent = 'Node Type: '; + nodeTypeLabel.style.display = 'block'; + nodeTypeLabel.style.marginBottom = '5px'; + + // Get node types from workflow + const workflowNodeTypes = new Set(); + if (app.graph && app.graph._nodes) { + for (const node of app.graph._nodes) { + workflowNodeTypes.add(node.type); + } + } + + const selectContainer = document.createElement('div'); + selectContainer.style.cssText = ` + position: relative; + width: 100%; + `; + + // Display selected value + const selectedDisplay = document.createElement('div'); + selectedDisplay.style.cssText = ` + width: 100%; + padding: 8px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + `; + + const updateSelectedDisplay = () => { + selectedDisplay.innerHTML = ''; + if (condition.node_type) { + if (workflowNodeTypes.has(condition.node_type)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + icon.style.fontWeight = 'bold'; + selectedDisplay.appendChild(icon); + } + const text = document.createElement('span'); + text.textContent = this.nodeTypes[condition.node_type]?.display_name || condition.node_type; + selectedDisplay.appendChild(text); + } else { + const text = document.createElement('span'); + text.textContent = '-- Select Node Type --'; + text.style.color = '#888'; + selectedDisplay.appendChild(text); + } + }; + + // Search input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search node types...'; + searchInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + display: none; + `; + + // Dropdown list + const dropdownList = document.createElement('div'); + dropdownList.style.cssText = ` + position: absolute; + width: 100%; + max-height: 200px; + overflow-y: auto; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + z-index: 1000; + display: none; + margin-top: 2px; + `; + + const nodeTypesList = Object.keys(this.nodeTypes).sort(); + + const filterDropdown = (searchTerm) => { + dropdownList.innerHTML = ''; + const filtered = nodeTypesList.filter(nodeType => { + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + return displayName.toLowerCase().includes(searchTerm.toLowerCase()) || + nodeType.toLowerCase().includes(searchTerm.toLowerCase()); + }); + + filtered.forEach(nodeType => { + const option = document.createElement('div'); + option.style.cssText = ` + padding: 8px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + `; + + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + icon.style.fontWeight = 'bold'; + option.appendChild(icon); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; + option.appendChild(nameSpan); + + option.addEventListener('mouseenter', () => { + option.style.background = '#007acc'; + }); + option.addEventListener('mouseleave', () => { + option.style.background = ''; + }); + option.addEventListener('click', () => { + condition.node_type = nodeType; + updateSelectedDisplay(); + updateAttributeOptions(nodeType); + dropdownList.style.display = 'none'; + searchInput.style.display = 'none'; + selectedDisplay.style.display = 'flex'; + }); + + dropdownList.appendChild(option); + }); + + dropdownList.style.display = filtered.length > 0 ? 'block' : 'none'; + }; + + selectedDisplay.addEventListener('click', () => { + selectedDisplay.style.display = 'none'; + searchInput.style.display = 'block'; + searchInput.focus(); + filterDropdown(''); + }); + + searchInput.addEventListener('input', () => { + filterDropdown(searchInput.value); + }); + + searchInput.addEventListener('blur', () => { + setTimeout(() => { + searchInput.style.display = 'none'; + selectedDisplay.style.display = 'flex'; + dropdownList.style.display = 'none'; + }, 200); + }); + + selectContainer.appendChild(selectedDisplay); + selectContainer.appendChild(searchInput); + selectContainer.appendChild(dropdownList); + + nodeTypeContainer.appendChild(nodeTypeLabel); + nodeTypeContainer.appendChild(selectContainer); + + // Attribute name dropdown (populated based on selected node) + const attrNameContainer = document.createElement('div'); + attrNameContainer.style.marginBottom = '10px'; + + const attrNameLabel = document.createElement('label'); + attrNameLabel.textContent = 'Attribute Name: '; + attrNameLabel.style.display = 'block'; + attrNameLabel.style.marginBottom = '5px'; + + const attrNameSelect = document.createElement('select'); + attrNameSelect.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + `; + + // Function to update attribute dropdown based on selected node type + const updateAttributeOptions = (nodeType) => { + attrNameSelect.innerHTML = ''; + + // Add empty option + const emptyOption = document.createElement('option'); + emptyOption.value = ''; + emptyOption.textContent = '-- Select Attribute --'; + attrNameSelect.appendChild(emptyOption); + + if (nodeType && this.nodeTypes[nodeType]) { + const nodeInfo = this.nodeTypes[nodeType]; + const inputs = nodeInfo.input?.required || {}; + const optionalInputs = nodeInfo.input?.optional || {}; + + // Add required inputs + for (const [inputName, inputDef] of Object.entries(inputs)) { + const option = document.createElement('option'); + option.value = inputName; + option.textContent = inputName; + if (condition.attribute_name === inputName) { + option.selected = true; + } + attrNameSelect.appendChild(option); + } + + // Add optional inputs + for (const [inputName, inputDef] of Object.entries(optionalInputs)) { + const option = document.createElement('option'); + option.value = inputName; + option.textContent = `${inputName} (optional)`; + if (condition.attribute_name === inputName) { + option.selected = true; + } + attrNameSelect.appendChild(option); + } + } + }; + + attrNameSelect.addEventListener('change', () => { + condition.attribute_name = attrNameSelect.value; + }); + + // Initialize displays + updateSelectedDisplay(); + updateAttributeOptions(condition.node_type); + + attrNameContainer.appendChild(attrNameLabel); + attrNameContainer.appendChild(attrNameSelect); + + // Attribute values + const attrValuesContainer = document.createElement('div'); + attrValuesContainer.style.marginBottom = '10px'; + + const attrValuesLabel = document.createElement('label'); + attrValuesLabel.textContent = 'Attribute Values (comma-separated): '; + attrValuesLabel.style.display = 'block'; + attrValuesLabel.style.marginBottom = '5px'; + + const attrValuesInput = document.createElement('input'); + attrValuesInput.type = 'text'; + attrValuesInput.value = (condition.attribute_values || []).join(', '); + attrValuesInput.placeholder = 'e.g., model1.safetensors, model2.ckpt'; + attrValuesInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + `; + + attrValuesInput.addEventListener('input', () => { + condition.attribute_values = attrValuesInput.value.split(',').map(s => s.trim()).filter(s => s); + }); + + attrValuesContainer.appendChild(attrValuesLabel); + attrValuesContainer.appendChild(attrValuesInput); + + container.appendChild(nodeTypeContainer); + container.appendChild(attrNameContainer); + container.appendChild(attrValuesContainer); + } + + createWorkflowNameFields(container, condition) { + const label = document.createElement('label'); + label.textContent = 'Workflow Names (comma-separated): '; + label.style.display = 'block'; + label.style.marginBottom = '5px'; + + const input = document.createElement('input'); + input.type = 'text'; + input.value = (condition.workflow_names || []).join(', '); + input.placeholder = 'e.g., portrait, landscape, character'; + input.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + `; + + input.addEventListener('input', () => { + condition.workflow_names = input.value.split(',').map(s => s.trim()).filter(s => s); + }); + + container.appendChild(label); + container.appendChild(input); + } + + createNodeInWorkflowFields(container, condition) { + const label = document.createElement('label'); + label.textContent = 'Node Types: '; + label.style.display = 'block'; + label.style.marginBottom = '5px'; + + // Multi-select dropdown with searchable autocomplete + const selectContainer = document.createElement('div'); + selectContainer.style.cssText = ` + position: relative; + width: 100%; + `; + + // Create a searchable input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search node types...'; + searchInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + margin-bottom: 5px; + `; + + // Selected items display + const selectedDisplay = document.createElement('div'); + selectedDisplay.style.cssText = ` + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-bottom: 10px; + min-height: 30px; + padding: 5px; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + `; + + // Dropdown list + const dropdownList = document.createElement('div'); + dropdownList.style.cssText = ` + position: absolute; + width: 100%; + max-height: 200px; + overflow-y: auto; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + z-index: 1000; + display: none; + `; + + // Get node types from workflow + const workflowNodeTypes = new Set(); + if (app.graph && app.graph._nodes) { + for (const node of app.graph._nodes) { + workflowNodeTypes.add(node.type); + } + } + + // Populate dropdown with all node types + const nodeTypesList = Object.keys(this.nodeTypes).sort(); + + const updateSelectedDisplay = () => { + selectedDisplay.innerHTML = ''; + (condition.node_types || []).forEach(nodeType => { + const tag = document.createElement('span'); + tag.style.cssText = ` + background: #007acc; + color: #fff; + padding: 3px 8px; + border-radius: 3px; + display: inline-flex; + align-items: center; + gap: 5px; + `; + + // Add icon if in workflow + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + tag.appendChild(icon); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType; + tag.appendChild(nameSpan); + + const removeBtn = document.createElement('button'); + removeBtn.textContent = '×'; + removeBtn.style.cssText = ` + background: transparent; + border: none; + color: #fff; + cursor: pointer; + padding: 0 3px; + font-size: 16px; + `; + removeBtn.addEventListener('click', () => { + condition.node_types = condition.node_types.filter(t => t !== nodeType); + updateSelectedDisplay(); + }); + + tag.appendChild(removeBtn); + selectedDisplay.appendChild(tag); + }); + }; + + const filterDropdown = (searchTerm) => { + dropdownList.innerHTML = ''; + const lowerSearch = searchTerm.toLowerCase(); + + const filtered = nodeTypesList.filter(nodeType => { + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + return displayName.toLowerCase().includes(lowerSearch) && + !(condition.node_types || []).includes(nodeType); + }); + + if (filtered.length === 0) { + dropdownList.style.display = 'none'; + return; + } + + filtered.forEach(nodeType => { + const option = document.createElement('div'); + option.style.cssText = ` + padding: 8px; + cursor: pointer; + border-bottom: 1px solid #555; + display: flex; + align-items: center; + gap: 5px; + `; + + // Add icon if in workflow + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + option.appendChild(icon); + } + + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + const textSpan = document.createElement('span'); + textSpan.textContent = displayName; + option.appendChild(textSpan); + + option.addEventListener('mouseover', () => { + option.style.background = '#007acc'; + }); + + option.addEventListener('mouseout', () => { + option.style.background = ''; + }); + + option.addEventListener('click', () => { + condition.node_types = condition.node_types || []; + condition.node_types.push(nodeType); + searchInput.value = ''; + updateSelectedDisplay(); + filterDropdown(''); + searchInput.focus(); + }); + + dropdownList.appendChild(option); + }); + + dropdownList.style.display = 'block'; + }; + + searchInput.addEventListener('focus', () => { + filterDropdown(searchInput.value); + }); + + searchInput.addEventListener('input', () => { + filterDropdown(searchInput.value); + }); + + searchInput.addEventListener('blur', () => { + setTimeout(() => { + dropdownList.style.display = 'none'; + }, 200); + }); + + updateSelectedDisplay(); + + selectContainer.appendChild(searchInput); + selectContainer.appendChild(dropdownList); + + container.appendChild(label); + container.appendChild(selectedDisplay); + container.appendChild(selectContainer); + } + + createAttributeInWorkflowFields(container, condition) { + const nodeTypeLabel = document.createElement('label'); + nodeTypeLabel.textContent = 'Node Type: '; + nodeTypeLabel.style.display = 'block'; + nodeTypeLabel.style.marginBottom = '5px'; + + // Node Type dropdown with search + const nodeTypeContainer = document.createElement('div'); + nodeTypeContainer.style.cssText = ` + position: relative; + width: 100%; + margin-bottom: 10px; + `; + + const nodeTypeInput = document.createElement('input'); + nodeTypeInput.type = 'text'; + nodeTypeInput.placeholder = 'Search node type...'; + nodeTypeInput.value = condition.node_type ? (this.nodeTypes[condition.node_type]?.display_name || condition.node_type) : ''; + nodeTypeInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + `; + + const nodeTypeDropdown = document.createElement('div'); + nodeTypeDropdown.style.cssText = ` + position: absolute; + width: 100%; + max-height: 200px; + overflow-y: auto; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 3px; + z-index: 1000; + display: none; + `; + + // Get node types from workflow + const workflowNodeTypes = new Set(); + if (app.graph && app.graph._nodes) { + for (const node of app.graph._nodes) { + workflowNodeTypes.add(node.type); + } + } + + const nodeTypesList = Object.keys(this.nodeTypes).sort(); + + const filterNodeTypeDropdown = (searchTerm) => { + nodeTypeDropdown.innerHTML = ''; + const lowerSearch = searchTerm.toLowerCase(); + + const filtered = nodeTypesList.filter(nodeType => { + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + return displayName.toLowerCase().includes(lowerSearch); + }); + + if (filtered.length === 0) { + nodeTypeDropdown.style.display = 'none'; + return; + } + + filtered.forEach(nodeType => { + const option = document.createElement('div'); + option.style.cssText = ` + padding: 8px; + cursor: pointer; + border-bottom: 1px solid #555; + display: flex; + align-items: center; + gap: 5px; + `; + + // Add icon if in workflow + if (workflowNodeTypes.has(nodeType)) { + const icon = document.createElement('span'); + icon.textContent = '✓'; + icon.style.color = '#0f0'; + option.appendChild(icon); + } + + const displayName = this.nodeTypes[nodeType]?.display_name || nodeType; + const textSpan = document.createElement('span'); + textSpan.textContent = displayName; + option.appendChild(textSpan); + + option.addEventListener('mouseover', () => { + option.style.background = '#007acc'; + }); + + option.addEventListener('mouseout', () => { + option.style.background = ''; + }); + + option.addEventListener('click', () => { + condition.node_type = nodeType; + nodeTypeInput.value = displayName; + nodeTypeDropdown.style.display = 'none'; + // Refresh attribute dropdown + updateAttributeDropdown(); + }); + + nodeTypeDropdown.appendChild(option); + }); + + nodeTypeDropdown.style.display = 'block'; + }; + + nodeTypeInput.addEventListener('focus', () => { + filterNodeTypeDropdown(nodeTypeInput.value); + }); + + nodeTypeInput.addEventListener('input', () => { + filterNodeTypeDropdown(nodeTypeInput.value); + }); + + nodeTypeInput.addEventListener('blur', () => { + setTimeout(() => { + nodeTypeDropdown.style.display = 'none'; + }, 200); + }); + + nodeTypeContainer.appendChild(nodeTypeInput); + nodeTypeContainer.appendChild(nodeTypeDropdown); + + // Attribute Name dropdown + const attrLabel = document.createElement('label'); + attrLabel.textContent = 'Attribute Name: '; + attrLabel.style.display = 'block'; + attrLabel.style.marginBottom = '5px'; + attrLabel.style.marginTop = '10px'; + + const attrSelect = document.createElement('select'); + attrSelect.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + margin-bottom: 10px; + `; + + const updateAttributeDropdown = () => { + attrSelect.innerHTML = ''; + + if (condition.node_type && this.nodeTypes[condition.node_type]) { + const attributes = this.nodeTypes[condition.node_type].attributes || {}; + Object.keys(attributes).forEach(attrName => { + const option = document.createElement('option'); + option.value = attrName; + option.textContent = attrName; + if (condition.attribute_name === attrName) { + option.selected = true; + } + attrSelect.appendChild(option); + }); + } + }; + + attrSelect.addEventListener('change', () => { + condition.attribute_name = attrSelect.value || null; + }); + + updateAttributeDropdown(); + + // Attribute Values + const valuesLabel = document.createElement('label'); + valuesLabel.textContent = 'Attribute Values (comma-separated, optional): '; + valuesLabel.style.display = 'block'; + valuesLabel.style.marginBottom = '5px'; + + const valuesInput = document.createElement('input'); + valuesInput.type = 'text'; + valuesInput.value = (condition.attribute_values || []).join(', '); + valuesInput.placeholder = 'e.g., value1, value2'; + valuesInput.style.cssText = ` + width: 100%; + padding: 5px; + background: #2a2a2a; + color: #fff; + border: 1px solid #555; + border-radius: 3px; + `; + + valuesInput.addEventListener('input', () => { + condition.attribute_values = valuesInput.value.split(',').map(s => s.trim()).filter(s => s); + }); + + container.appendChild(nodeTypeLabel); + container.appendChild(nodeTypeContainer); + container.appendChild(attrLabel); + container.appendChild(attrSelect); + container.appendChild(valuesLabel); + container.appendChild(valuesInput); + } + + async saveCurrentNote() { + if (!this.currentEditingNote) return false; + + // Clean up trigger conditions - remove null values + const cleanedTriggerConditions = (this.currentTriggerConditions || []).map(tc => { + const cleaned = { type: tc.type }; + if (tc.node_types !== null && tc.node_types !== undefined) cleaned.node_types = tc.node_types; + if (tc.node_type !== null && tc.node_type !== undefined) cleaned.node_type = tc.node_type; + if (tc.attribute_name !== null && tc.attribute_name !== undefined) cleaned.attribute_name = tc.attribute_name; + if (tc.attribute_values !== null && tc.attribute_values !== undefined) cleaned.attribute_values = tc.attribute_values; + if (tc.workflow_names !== null && tc.workflow_names !== undefined) cleaned.workflow_names = tc.workflow_names; + return cleaned; + }); + + const data = { + content: this.contentTextarea.value, + format_style: this.formatSelect.value, + pinned: this.pinnedCheckbox.checked, + trigger_conditions: cleanedTriggerConditions + }; + + try { + const response = await api.fetchApi(`/autonotes/notes/${this.currentEditingNote.uuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + const result = await response.json(); + if (result.success) { + // Update local copy + Object.assign(this.currentEditingNote, data); + await this.refreshNotes(); + return true; + } else { + alert('Failed to save note'); + return false; + } + } catch (error) { + console.error('Failed to save note:', error); + alert('Failed to save note'); + return false; + } + } + + async closeEditDialog(save = false) { + if (save && this.currentEditingNote) { + await this.saveCurrentNote(); + } + + if (this.editDialog) { + this.editDialog.parentElement.remove(); // Remove overlay + this.editDialog = null; + this.currentEditingNote = null; + } + } + + async togglePin(note) { + try { + const newPinnedState = !note.pinned; + const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: note.content, + format_style: note.format_style, + pinned: newPinnedState, + trigger_conditions: note.trigger_conditions || [] + }), + }); + + const result = await response.json(); + if (result.success) { + note.pinned = newPinnedState; + await this.refreshNotes(); + } else { + alert('Failed to toggle pin'); + } + } catch (error) { + console.error('Failed to toggle pin:', error); + alert('Failed to toggle pin'); + } + } + + async addFolder(treeContainer, selectedNoteUuid) { + const name = prompt('Enter folder name:'); + if (!name) return; + + try { + const response = await api.fetchApi('/autonotes/folders', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name }), + }); + + const result = await response.json(); + if (result.uuid) { + await this.loadFolders(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + } + } catch (error) { + console.error('Failed to create folder:', error); + alert('Failed to create folder'); + } + } + + async renameFolder(folder, treeContainer, selectedNoteUuid) { + const newName = prompt('Enter new folder name:', folder.name); + if (!newName || newName === folder.name) return; + + try { + const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: newName }), + }); + + const result = await response.json(); + if (result.success) { + await this.loadFolders(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + } else { + alert('Failed to rename folder'); + } + } catch (error) { + console.error('Failed to rename folder:', error); + alert('Failed to rename folder'); + } + } + + async deleteFolder(folder, treeContainer, selectedNoteUuid) { + if (!confirm(`Delete folder "${folder.name}"? Notes inside will not be deleted.`)) { + return; + } + + try { + const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, { + method: 'DELETE', + }); + + const result = await response.json(); + if (result.success) { + await this.loadFolders(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + } else { + alert('Failed to delete folder'); + } + } catch (error) { + console.error('Failed to delete folder:', error); + alert('Failed to delete folder'); + } + } + + async moveNoteToFolder(noteUuid, folderUuid, treeContainer, selectedNoteUuid) { + try { + const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ folder_uuid: folderUuid }), + }); + + const result = await response.json(); + if (result.success) { + await this.refreshNotes(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + } else { + alert('Failed to move note'); + } + } catch (error) { + console.error('Failed to move note:', error); + alert('Failed to move note'); + } + } + + showNoteContextMenu(event, note, treeContainer, selectedNoteUuid) { + // Remove any existing context menu + const existingMenu = document.querySelector('.note-context-menu'); + if (existingMenu) { + existingMenu.remove(); + } + + const menu = document.createElement('div'); + menu.className = 'note-context-menu'; + menu.style.cssText = ` + position: fixed; + left: ${event.clientX}px; + top: ${event.clientY}px; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 4px; + padding: 5px 0; + z-index: 10000; + min-width: 150px; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + `; + + const renameOption = document.createElement('div'); + renameOption.textContent = 'Rename'; + renameOption.style.cssText = ` + padding: 8px 15px; + cursor: pointer; + color: #fff; + `; + renameOption.addEventListener('mouseenter', () => { + renameOption.style.background = '#007acc'; + }); + renameOption.addEventListener('mouseleave', () => { + renameOption.style.background = ''; + }); + renameOption.addEventListener('click', async () => { + menu.remove(); + const newName = prompt('Enter new note name:', note.name); + if (newName && newName !== note.name) { + await this.renameNote(note.uuid, newName, treeContainer, selectedNoteUuid); + } + }); + + const deleteOption = document.createElement('div'); + deleteOption.textContent = 'Delete'; + deleteOption.style.cssText = ` + padding: 8px 15px; + cursor: pointer; + color: #fff; + `; + deleteOption.addEventListener('mouseenter', () => { + deleteOption.style.background = '#cc0000'; + }); + deleteOption.addEventListener('mouseleave', () => { + deleteOption.style.background = ''; + }); + deleteOption.addEventListener('click', async () => { + menu.remove(); + if (confirm(`Delete note "${note.name}"?`)) { + await this.deleteNote(note.uuid, treeContainer, selectedNoteUuid); + } + }); + + menu.appendChild(renameOption); + menu.appendChild(deleteOption); + document.body.appendChild(menu); + + // Close menu when clicking outside + const closeMenu = (e) => { + if (!menu.contains(e.target)) { + menu.remove(); + document.removeEventListener('click', closeMenu); + } + }; + setTimeout(() => document.addEventListener('click', closeMenu), 0); + } + + async renameNote(noteUuid, newName, treeContainer, selectedNoteUuid) { + try { + const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: newName }), + }); + + const result = await response.json(); + if (result.success) { + await this.refreshNotes(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + } else { + alert('Failed to rename note'); + } + } catch (error) { + console.error('Failed to rename note:', error); + alert('Failed to rename note'); + } + } + + async deleteNote(noteUuid, treeContainer, selectedNoteUuid) { + try { + const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { + method: 'DELETE', + }); + + const result = await response.json(); + if (result.success) { + await this.refreshNotes(); + this.renderNoteTree(treeContainer, selectedNoteUuid); + // Clear editor if deleted note was being edited + if (this.currentEditingNote && this.currentEditingNote.uuid === noteUuid) { + this.currentEditingNote = null; + if (this.contentTextarea) { + this.contentTextarea.value = ''; + } + } + } else { + alert('Failed to delete note'); + } + } catch (error) { + console.error('Failed to delete note:', error); + alert('Failed to delete note'); + } + } + + async updateNoteContent(noteUuid, content) { + try { + const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content }), + }); + + const result = await response.json(); + if (result.success) { + // Refresh to show updated content + await this.refreshNotes(); + } else { + alert('Failed to save note'); + } + } catch (error) { + console.error('Failed to save note:', error); + alert('Failed to save note'); + } + } +} + +// Initialize the AutoNotes manager when the extension loads +app.registerExtension({ + name: "AutoNotes", + async setup() { + new AutoNotesManager(); + }, +}); \ No newline at end of file diff --git a/web/marked.min.js b/web/marked.min.js new file mode 100644 index 0000000..789fc6b --- /dev/null +++ b/web/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v11.1.1 - a markdown parser + * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull )((?:.|\n(?!\s*?\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$)/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),Z={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},q=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...Z,table:q,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",q).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...Z,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}$+<=>`^|~",M=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),O=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),C=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:O,emStrongRDelimAst:C,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:M,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{n=n.concat(this.walkTokens(e[s],t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke}));