commit fd53d6fdfd56f52fc32172717f6920ed4d7c15df Author: daxcay Date: Sat Aug 24 08:51:51 2024 +0530 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f45a09a --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyc +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +*.pyc + +.idea +/node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e550b32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 daxcay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..56ea248 --- /dev/null +++ b/__init__.py @@ -0,0 +1,419 @@ +import logging +import json +import os +import shutil +import datetime +import uuid +from aiohttp import web +from server import PromptServer + +from .lib.WebSocketManager import WebSocketManager + +DATA_FOLDER = './nexus' + +PERMISSIONS_FILE = os.path.join(DATA_FOLDER, 'permissions.json') +ADMINS_FILE = os.path.join(DATA_FOLDER, 'admins.json') +TOKENS_FILE = os.path.join(DATA_FOLDER, 'tokens.json') +NAMES_FILE = os.path.join(DATA_FOLDER, 'names.json') + +SERVER_FILE = os.path.join(DATA_FOLDER, 'server.json') +WORKFLOWS_DIR = os.path.join(DATA_FOLDER, 'workflows') + +if not os.path.exists(DATA_FOLDER): + os.makedirs(DATA_FOLDER) + +if not os.path.exists(ADMINS_FILE): + with open(ADMINS_FILE, 'w') as f: + admins_data = {'epic': 'comfynexus'} + json.dump(admins_data, f) + +for file_path in [PERMISSIONS_FILE, ADMINS_FILE, TOKENS_FILE, NAMES_FILE]: + if not os.path.exists(file_path): + with open(file_path, 'w') as f: + json.dump({}, f) + +if not os.path.exists(WORKFLOWS_DIR): + os.makedirs(WORKFLOWS_DIR) + +if not os.path.exists(SERVER_FILE): + with open(SERVER_FILE, 'w') as f: + default_config = { + "name": "Default Nexus Server", + "max_user": 15, + "config": { + "last_saved_user_id": "id" + } + } + json.dump(default_config, f, indent=4) + + +if hasattr(PromptServer, 'instance'): + server = PromptServer.instance + routes = web.RouteTableDef() + socket_manager = WebSocketManager() + + async def broadcast_message(message, sender): + for id, socket in socket_manager.sockets.items(): + if id != sender: + try: + await socket.send_str(json.dumps(message)) + except Exception as e: + logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}") + + async def broadcast_message_except(message, excluder): + for id, socket in socket_manager.sockets.items(): + if id != excluder: + try: + await socket.send_str(json.dumps(message)) + except Exception as e: + logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}") + + async def broadcast_message_all(message): + for id, socket in socket_manager.sockets.items(): + try: + await socket.send_str(json.dumps(message)) + except Exception as e: + logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}") + + async def send_message(message, sender, receiver): + for id, socket in socket_manager.sockets.items(): + if id == receiver and sender != receiver: + try: + await socket.send_str(json.dumps(message)) + except Exception as e: + logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}") + + @routes.get("/nexus") + async def websocket_handler(request): + id = request.query.get("id") + socket = web.WebSocketResponse() + await socket.prepare(request) + + socket_manager.set(id, socket) + logging.info(f"[ComfyUI-Nexus]: Socket {id} connected") + + try: + async for msg in socket: + if msg.type == web.WSMsgType.TEXT: + + data = json.loads(msg.data) + event_name = data.get('name') + event_data = data.get('data') + + sender = data.get("from") + all = data.get('all') + receiver = data.get('receiver') + exclude = data.get('exclude') + + # logging.info(f"[ComfyUI-Nexus]: Received {sender} {all} {receiver} {exclude}") + + if event_name == "chat-join": + user_id = data.get("from") + user_name = event_data.get("name") + + with open(NAMES_FILE, 'r+') as f: + names = json.load(f) + names[user_id] = user_name + f.seek(0) + json.dump(names, f, indent=4) + f.truncate() + + if event_name == "nick": + user_id = data.get("from") + user_name = event_data.get("new_name") + + with open(NAMES_FILE, 'r+') as f: + names = json.load(f) + names[user_id] = user_name + f.seek(0) + json.dump(names, f, indent=4) + f.truncate() + + if all: + await broadcast_message_all(data) + elif receiver: + await send_message(data, sender, receiver) + elif exclude: + await broadcast_message_except(data, exclude) + else: + await broadcast_message(data, sender) + + elif msg.type == web.WSMsgType.ERROR: + logging.error(f"[ComfyUI-Nexus]: WebSocket error: {msg.exception()}") + except Exception as e: + logging.error(f"[ComfyUI-Nexus]: WebSocket error: {e}") + finally: + logging.info(f"[ComfyUI-Nexus]: Socket {id} disconnected") + socket_manager.delete(id) + + return socket + + @routes.post("/nexus/workflows") + async def get_user_specific_workflows(request): + + user_id = (await request.json()).get('user_id') + if not user_id: + return web.json_response({'error': 'User ID required'}, status=400) + + with open(PERMISSIONS_FILE, 'r') as f: + permissions = json.load(f) + + user_permissions = permissions.get(user_id, {}) + if not user_permissions.get('editor', False): + return web.json_response({'error': 'Forbidden'}, status=403) + + user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id) + if not os.path.exists(user_workflow_dir): + return web.json_response({'error': 'User not found'}, status=404) + + workflows = {} + for file_name in sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int): + file_path = os.path.join(user_workflow_dir, file_name) + with open(file_path, 'r') as f: + workflows[file_name] = json.load(f) + + return web.json_response(workflows) + + @routes.post("/nexus/workflows/meta") + async def get_user_specific_workflows_meta(request): + + user_id = (await request.json()).get('user_id') + if not user_id: + return web.json_response({'error': 'User ID required'}, status=400) + with open(PERMISSIONS_FILE, 'r') as f: + permissions = json.load(f) + + user_permissions = permissions.get(user_id, {}) + if not user_permissions.get('editor', False): + return web.json_response({'error': 'Forbidden'}, status=403) + + user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id) + if not os.path.exists(user_workflow_dir): + return web.json_response({'error': 'User not found'}, status=404) + + meta_data = [] + for file_name in sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int): + file_path = os.path.join(user_workflow_dir, file_name) + last_modified_time = os.path.getmtime(file_path) + last_modified_readable = datetime.datetime.fromtimestamp(last_modified_time).strftime('%Y-%m-%d %H:%M:%S') + + meta_data.append({ + 'file_name': file_name, + 'last_saved': last_modified_readable + }) + + return web.json_response(meta_data) + + @routes.post("/nexus/workflows/{id}") + async def get_user_specific_workflow(request): + user_id = (await request.json()).get('user_id') + workflow_id = request.match_info.get('id') + + if not user_id: + return web.json_response({'error': 'User ID required'}, status=400) + if not workflow_id: + return web.json_response({'error': 'Workflow ID required'}, status=400) + + with open(PERMISSIONS_FILE, 'r') as f: + permissions = json.load(f) + + user_permissions = permissions.get(user_id, {}) + if not user_permissions.get('editor', False): + return web.json_response({'error': 'Forbidden'}, status=403) + + user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id) + if not os.path.exists(user_workflow_dir): + return web.json_response({'error': 'User not found'}, status=404) + + workflow_file_path = os.path.join(user_workflow_dir, workflow_id) + if not os.path.exists(workflow_file_path): + return web.json_response({'error': 'Workflow not found'}, status=404) + + with open(workflow_file_path, 'r') as f: + workflow = json.load(f) + + return web.json_response(workflow) + + @routes.post("/nexus/workflow") + async def post_workflow(request): + user_id = (await request.json()).get('user_id') + if not user_id: + return web.json_response({'error': 'User ID required'}, status=400) + + with open(PERMISSIONS_FILE, 'r') as f: + permissions = json.load(f) + + user_permissions = permissions.get(user_id, {}) + if not user_permissions.get('editor', False): + return web.json_response({'error': 'Forbidden'}, status=403) + + new_data = await request.json() + + user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id) + if not os.path.exists(user_workflow_dir): + os.makedirs(user_workflow_dir) + + existing_files = sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int) + + if len(existing_files) == 5: + os.remove(os.path.join(user_workflow_dir, existing_files[0])) + existing_files.pop(0) + + for i, file_name in enumerate(existing_files): + new_name = str(i + 1) + shutil.move(os.path.join(user_workflow_dir, file_name), os.path.join(user_workflow_dir, new_name)) + + new_file_path = os.path.join(user_workflow_dir, '5') + with open(new_file_path, 'w') as f: + json.dump(new_data, f, indent=4) + + # Update the server.json file with the last saved user_id + with open(SERVER_FILE, 'r+') as f: + server_config = json.load(f) + server_config['config']['last_saved_user_id'] = user_id + f.seek(0) + json.dump(server_config, f, indent=4) + f.truncate() + + return web.json_response({'status': 'success'}, status=200) + + @routes.get("/nexus/workflow") + async def get_latest_workflow(request): + with open(SERVER_FILE, 'r') as f: + server_config = json.load(f) + + last_saved_user_id = server_config['config'].get('last_saved_user_id') + if not last_saved_user_id: + return web.json_response({'error': 'No workflow saved'}, status=404) + + user_workflow_dir = os.path.join(WORKFLOWS_DIR, last_saved_user_id) + if not os.path.exists(user_workflow_dir): + return web.json_response({'error': 'User not found'}, status=404) + + last_file_path = os.path.join(user_workflow_dir, '5') + if not os.path.exists(last_file_path): + return web.json_response({'error': 'Workflow not found'}, status=404) + + with open(last_file_path, 'r') as f: + workflow_data = json.load(f) + + return web.json_response(workflow_data) + + @routes.get("/nexus/permission/{id}") + async def get_permission_by_id(request): + user_id = request.match_info['id'] + with open(PERMISSIONS_FILE, 'r') as f: + data = json.load(f) + user_permission = data.get(user_id) + if user_permission: + return web.json_response(user_permission) + else: + return web.json_response({'error': 'User not found'}, status=404) + + @routes.post("/nexus/permission/{id}") + async def post_permission_by_id(request): + user_id = request.match_info['id'] + data = await request.json() + admin_id = data.get('admin_id') + if not admin_id: + return web.json_response({'error': 'Admin ID required'}, status=400) + + token = request.headers.get('Authorization') + if not token: + return web.json_response({'error': 'Authorization token required'}, status=400) + + with open(TOKENS_FILE, 'r') as f: + tokens = json.load(f) + + if tokens.get(token) != admin_id: + return web.json_response({'error': 'Invalid or expired token'}, status=403) + + new_permission = data.get('data') + if not new_permission: + return web.json_response({'error': 'Permission data required'}, status=400) + + with open(PERMISSIONS_FILE, 'r+') as f: + permissions = json.load(f) + + if not permissions.get(admin_id, {}).get('admin', False): + return web.json_response({'error': 'Forbidden'}, status=403) + + # Merge the new permissions with the existing ones + existing_permissions = permissions.get(user_id, {}) + existing_permissions.update(new_permission) + + # Save the updated permissions + permissions[user_id] = existing_permissions + f.seek(0) + json.dump(permissions, f, indent=4) + f.truncate() + + return web.json_response({'status': 'success'}, status=200) + + @routes.post("/nexus/login") + async def post_login(request): + + credentials = await request.json() + user_id = credentials.get('uuid') + account = credentials.get('account') + password = credentials.get('password') + + with open(ADMINS_FILE, 'r') as f: + admins = json.load(f) + + if admins.get(account) == password: + token = str(uuid.uuid4()) + + with open(TOKENS_FILE, 'r+') as f: + tokens = json.load(f) + tokens[token] = user_id + f.seek(0) + json.dump(tokens, f, indent=4) + f.truncate() + + with open(PERMISSIONS_FILE, 'r+') as f: + permissions = json.load(f) + permissions[user_id] = {'editor': True, 'admin': True} + f.seek(0) + json.dump(permissions, f, indent=4) + f.truncate() + + return web.json_response({'token': token}, status=200) + + else: + return web.json_response({'error': 'Invalid credentials'}, status=401) + + @routes.post("/nexus/verify") + async def post_verify(request): + credentials = await request.json() + token = credentials.get('token') + + with open(TOKENS_FILE, 'r') as f: + tokens = json.load(f) + + user_id = tokens.get(token) + if user_id: + with open(PERMISSIONS_FILE, 'r') as f: + permissions = json.load(f) + + user_permissions = permissions.get(user_id, {}) + return web.json_response({'user_id': user_id, 'permissions': user_permissions}, status=200) + else: + return web.json_response({'error': 'Invalid token'}, status=401) + + @routes.get("/nexus/name/{id}") + async def get_name_by_id(request): + user_id = request.match_info['id'] + with open(NAMES_FILE, 'r') as f: + names = json.load(f) + user_name = names.get(user_id) + if user_name: + return web.json_response({'name': user_name}) + else: + return web.json_response({'error': 'User not found'}, status=404) + + server.app.add_routes(routes) + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} +WEB_DIRECTORY = "./web" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4d0bca0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "comfyui-nexus" +description = "Seamless Multiuser Workflow Collaboration" +version = "1.0" +license = "LICENSE" +dependencies = [] + +[project.urls] +Repository = "https://github.com/daxcay/ComfyUI-Nexus" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "daxcay" +DisplayName = "ComfyUI-Nexus" +Icon = "https://raw.githubusercontent.com/daxcay/ComfyUI-Nexus/master/web/assets/img/logo.jpg" diff --git a/web/assets/img/logo.png b/web/assets/img/logo.png new file mode 100644 index 0000000..01d6e64 Binary files /dev/null and b/web/assets/img/logo.png differ diff --git a/web/util.js b/web/util.js new file mode 100644 index 0000000..ebfdba2 --- /dev/null +++ b/web/util.js @@ -0,0 +1,11 @@ +(()=>{var E=class{constructor(){this.users={}}create(e){this.users[e]||(this.users[e]={})}set(e,t,s){this.create(e),this.users[e][t]=s}get(e,t){return this.create(e),this.users[e][t]}remove(e){return this.users[e]&&delete this.users[e],!0}};var N=class extends EventTarget{getName(){let e=localStorage.getItem("nexus-socket-name");return e||(localStorage.setItem("nexus-socket-name","User"),e="User"),this.userManager.set(this.uuid,"name",e),e}getUUID(){let e=localStorage.getItem("nexus-socket-uuid");return e||(e="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>(Math.random()*16|0).toString(16)),localStorage.setItem("nexus-socket-uuid",e)),e}constructor(){super(),this.ws=null,this.userManager=new E,this.uuid=this.getUUID(),this.admin=!1,this.wsUrl=`ws${window.location.protocol==="https:"?"s":""}://${location.host}/nexus?id=${this.uuid}`,this.onConnected=[this.onConnectedHandler.bind(this)],this.onDisconnected=[],this.onMessageReceive=[this.onMessageReceiveHandler.bind(this)],this.onMessageSend=[],this.onError=[],this.onClose=[],this.getName()}onConnectedHandler(){this.userManager.create(this.uuid),this.sendMessage("join",{name:this.getName()},!1),this.sendMessage("online",{},!1)}onMessageReceiveHandler(e){let t=e.name,s=e.from;switch(t){case"join":this.userManager.create(s),this.sendMessage("hi",{},!1);break;case"hi":this.userManager.create(s);break}}connect(){try{this.ws=new WebSocket(this.wsUrl),this.ws.onopen=()=>{this.onConnected&&this.onConnected.forEach(e=>{e()})},this.ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessageReceive&&this.onMessageReceive.forEach(s=>{s(t)})}catch(t){console.error("Error handling message:",t)}},this.ws.onerror=e=>{this.onError&&this.onError.forEach(t=>{t(e)}),console.error("WebSocket error:",e)},this.ws.onclose=()=>{this.onDisconnected&&this.onDisconnected.forEach(e=>{e()}),setTimeout(()=>{this.connect()},5e3)}}catch(e){console.error("Error connecting to WebSocket:",e)}}closeWebSocket(){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.close():console.log("WebSocket is not open.")}sendMessage(e,t,s=!0){try{if(this.ws&&this.ws.readyState===WebSocket.OPEN){let n={};t.receiver?(n.receiver=t.receiver,delete t.receiver):t.exclude&&(n.exclude=t.exclude,delete t.exclude),n.from=this.uuid,n.name=e,n.data=t,n.all=s,this.ws.send(JSON.stringify(n)),this.onMessageSend&&this.onMessageSend.forEach(o=>{o(message)})}}catch(n){console.error(`Error sending ${e} message:`,n)}}};var L=class{hexColors=["#1abc9c","#2ecc71","#3498db","#9b59b6","#f1c40f","#e67e22","#e74c3c","#ecf0f1"];createMouseSvg(e){return` + + `}svgStringToImage(e){let t=new Blob([e],{type:"image/svg+xml"}),s=URL.createObjectURL(t),n=new Image;return n.src=s,n}hex2rgb(e){let t=parseInt(e.slice(1,3),16),s=parseInt(e.slice(3,5),16),n=parseInt(e.slice(5,7),16);return{r:t,g:s,b:n}}getRandomHexColor(){return this.hexColors[Math.floor(Math.random()*this.hexColors.length)]}constructor(e,t,s){this.color=this.getRandomHexColor(),this.app=t,this.fps=s,this.canSendMouse=!1,this.nexusSocket=e,this.nexusSocket.onConnected.push(this.handleConnect.bind(this)),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),document.addEventListener("visibilitychange",this.handleVisiblityChange.bind(this)),document.addEventListener("mousemove",this.handleSendMouseControl.bind(this)),this.app.graph.list_of_graphcanvas[0].onDrawForeground=this.handlePointerDraw.bind(this),setInterval(this.handleSendMouse.bind(this),1e3/this.fps)}throttle(e,t){let s,n;return function(...o){let a=this;n?(clearTimeout(s),s=setTimeout(function(){Date.now()-n>=t&&(e.apply(a,o),n=Date.now())},t-(Date.now()-n))):(e.apply(a,o),n=Date.now())}}handleCanvasDraw(){this.app.graph&&this.app.graph.list_of_graphcanvas&&this.app.graph.list_of_graphcanvas.length>0&&this.app.graph.list_of_graphcanvas[0].draw(!0,!0)}handleConnect(){this.drawTimer=setInterval(this.handleCanvasDraw.bind(this),1e3/this.fps)}handleMessageReceive(e){let t=e.name,s=e.from,n=e.data;if(t=="mouse"){let o=this.nexusSocket.userManager.get(s,"mouse"),a=this.nexusSocket.userManager.get(s,"pointer");a?a&&o&&o[3]!=n.mouse[3]&&this.nexusSocket.userManager.set(s,"pointer",this.svgStringToImage(this.createMouseSvg(n.mouse[3]))):this.nexusSocket.userManager.set(s,"pointer",this.svgStringToImage(this.createMouseSvg(n.mouse[3]))),this.nexusSocket.userManager.set(s,"mouse",n.mouse)}else t=="hide_mouse"?this.nexusSocket.userManager.set(s,"mouse",null):t=="mouse_view_toggle"&&(this.nexusSocket.userManager.set(s,"mouse_view",n.on),n.on==0&&this.nexusSocket.sendMessage("hide_mouse",{receiver:s},!1))}handleVisiblityChange(){document.visibilityState==="hidden"?(clearInterval(this.drawTimer),this.nexusSocket.sendMessage("hide_mouse",{},!1),this.nexusSocket.sendMessage("afk",{},!1)):(this.handleConnect(),this.nexusSocket.sendMessage("online",{},!1))}handleSendMouse=function(){if(this.app.graph&&this.app.graph.list_of_graphcanvas&&this.app.graph.list_of_graphcanvas.length>0&&(this.mouse=this.app.graph.list_of_graphcanvas[0].canvas_mouse,this.scale=this.app.graph.list_of_graphcanvas[0].ds.scale),this.mouse[3]=this.color,this.mouse[4]=this.scale/10,this.canSendMouse){let e=Object.keys(this.nexusSocket.userManager.users);for(let t=0;t{if(t!=this.nexusSocket.uuid){let s=this.nexusSocket.userManager.get(t,"mouse"),n=this.nexusSocket.userManager.get(t,"name")||"User",o=this.nexusSocket.userManager.get(t,"pointer");if(this.nexusSocket.userManager.get(t,"mouse_hidden")==0&&(s=null),s&&s.length>0){let r=s[0]-4,h=s[1]-4,l=1-s[4];e.drawImage(o,r,h,18,18),e.font="14px Arial",e.fillStyle=s[3],e.fillText(n,r+22,h+16);let d=this.hex2rgb(s[3]);e.beginPath(),e.arc(r,h,l*600,0,2*Math.PI,!1),e.strokeStyle=`rgba(${d.r},${d.g},${d.b},0.1)`,e.lineWidth=2,e.stroke()}}})}};LiteGraph.use_uuids=!0;LiteGraph.uuidv4=function(){return Date.now()};LGraph.prototype.getGroupByUUID=function(i){let e=this._groups,t=null;for(let s=0;s=LiteGraph.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";if(LiteGraph.use_uuids){(i.id==null||i.id==-1)&&(i.id=LiteGraph.uuidv4());let t=localStorage.getItem("nexus-socket-uuid");t&&(i.properties.owner=t),this.last_node_id=i.id}else i.id==null||i.id==-1?i.id=++this.last_node_id:this.last_node_id=LiteGraph.MAX_NUMBER_OF_NODES)throw"LiteGraph: max number of nodes in a graph reached";return this.last_node_id=i.id,i.graph=this,this._version++,this._nodes.push(i),this._nodes_by_id[i.id]=i,this.config.align_to_grid&&i.alignToGrid(),e||this.updateExecutionOrder(),this.onNodeAddedNexus&&this.onNodeAddedNexus(i),this.setDirtyCanvas(!0),this.change(),i}};LGraph.prototype.remove=function(i){if(i.constructor===LiteGraph.LGraphGroup){var e=this._groups.indexOf(i);e!=-1&&this._groups.splice(e,1),i.graph=null,this._version++,this.setDirtyCanvas(!0,!0),this.change(),this.onGroupRemoved&&this.onGroupRemoved(i);return}if(this._nodes_by_id[i.id]!=null&&!i.ignore_remove){if(this.beforeChange(),i.inputs)for(var t=0;tthis.updateNode(e))};LGraph.prototype.updateLinks=function(i){i.forEach(e=>this.updateLink(e))};LGraph.prototype.updateGroups=function(i){i.forEach(e=>this.updateGroup(e))};var x="nexus";async function y(i,e={}){try{let t=await fetch(i,e),s=await t.json();if(!t.ok)throw new Error(s.error||"Something went wrong");return s}catch(t){return console.error(`Fetch error: ${t.message}`),{error:t.message}}}async function F(i,e){let t=`${x}/workflows/${e}`,s={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i})};return await y(t,s)}async function $(i){let e=`${x}/workflows/meta`,t={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i})};return await y(e,t)}async function U(i,e){let t=`${x}/workflow`,s={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:i,...e})};return await y(t,s)}async function D(){let i=`${x}/workflow`;return await y(i)}async function O(i){let e=`${x}/permission/${i}`;return await y(e)}async function G(i,e,t,s){let n=`${x}/permission/${i}`,o={method:"POST",headers:{"Content-Type":"application/json",Authorization:s},body:JSON.stringify({admin_id:e,data:t})};return await y(n,o)}async function H(i,e,t){let s=`${x}/login`,n={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({uuid:i,account:e,password:t})};return await y(s,n)}async function B(i){let e=`${x}/verify`,t={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:i})};return await y(e,t)}var A=class{constructor(e,t,s){this.app=e,this.api=t,this.nexusSocket=s,this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.app.graph.clear(),this.spectators=[],this.saveTime=0,setInterval(this.handleSpectators.bind(this),1e3/24)}removeSaveManager(){this.saveTimer&&clearInterval(this.saveTimer)}async saveManager(e){this.nexusSocket.admin&&this.nexusSocket.sendMessage("workflow:timer",{time:e-2},!1),this.removeSaveManager(),this.saveTime=e,this.saveTimer=setInterval(this.sendWorkFlow.bind(this),e*1e3)}async sendWorkFlow(){let e=(await this.app.graphToPrompt()).workflow;await U(this.nexusSocket.uuid,e)}async loadLatestWorkflow(){let e=await D(),t=e.links||[],s=e.nodes||[],n=e.groups||[];this.app.graph.updateNodes(s),this.app.graph.updateLinks(t),this.app.graph.updateGroups(n),this.app.graph.updateExecutionOrder(),this.app.graph.setDirtyCanvas(!0,!0)}async handleSaveWorkflowManager(){document.visibilityState==="hidden"?this.saveTimer&&clearInterval(this.saveTimer):this.saveTimer=setInterval(this.sendWorkFlow.bind(this),this.saveTime*1e3)}async handleSpectators(){if(this.spectators.length>0){let t=(await this.app.graphToPrompt()).workflow.extra;for(let s=0;s{t.onGroupMoved=this.onGroupMoved.bind(this,t),t.onGroupNodeMoved=this.onNodeMoved.bind(this)}),this.app.graph._nodes.forEach(t=>{t.onWidgetChanged=this.onWidgetChanged.bind(this,t),t.onConnectionsChange=this.onNodeConnectionChange.bind(this,t)})):(this.app.graph.onNodeAddedNexus=null,this.app.graph.onNodeAdded=null,this.app.graph.onNodeRemoved=null,this.app.canvas.onSelectionChange=null,this.app.canvas.onNodeDeselected=null,this.app.canvas.onNodeMoved=null,this.app.graph.onGroupAdded=null,this.app.graph.onGroupRemoved=null,this.app.graph._groups.forEach(t=>{t.onGroupMoved=null,t.onGroupNodeMoved=null}),this.app.graph._nodes.forEach(t=>{t.onWidgetChanged=null,t.onConnectionsChange=null}))}handleWorkflowUpdate(e,t){let s=this.app.graph,n=this.app.canvas,o=t.update,a=t.data,r;switch(o){case"node-added":s.updateNode(a);break;case"node-removed":s.removeNode(a);break;case"node-moved":s.updateNode(a);break;case"node-widget-changed":s.updateNode(a);break;case"node-selection":let h=[];a.forEach(l=>{r=s.getNodeById(l.id),r&&h.push(r)}),n.selectNodesNexus(h,!0);break;case"node-deselection":r=s.getNodeById(a.id),r&&n.deselectNodeNexus(r);break;case"group-added":s.updateGroup(a);break;case"group-moved":s.updateGroup(a);break;case"group-removed":s.removeGroup(a);break;case"node-connection":s.updateLinkManual(t.change,t.link,t.node,s);break;default:break}}onNodeAddedNexus(e){e.onWidgetChanged=this.onWidgetChanged.bind(this,e),e.onConnectionsChange=this.onNodeConnectionChange.bind(this,e)}onNodeAdded(e){let t=e.serialize();e.onWidgetChanged=this.onWidgetChanged.bind(this,e),e.onConnectionsChange=this.onNodeConnectionChange.bind(this,e),this.nexusSocket.sendMessage("workflow:update",{update:"node-added",data:t},!1)}onWidgetChanged(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-widget-changed",data:t},!1)}onNodeRemoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-removed",data:t},!1)}async onNodeConnectionChange(e,t,s,n,o,a){let r=n?"add":"remove",h=o;this.graphData=(await this.app.graphToPrompt()).workflow;let l=this.graphData.links;e=this.app.graph.getNodeById(e.id),e=e?e.serialize():null,this.nexusSocket.sendMessage("workflow:update",{node:e,update:"node-connection",link:h,change:r,data:l},!1)}onSelectionChange(e){let t=[];Object.keys(e).forEach(s=>{t.push(e[s].serialize())}),this.nexusSocket.sendMessage("workflow:update",{update:"node-selection",data:t},!1)}onNodeDeselected(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-deselection",data:t},!1)}onNodeMoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"node-moved",data:t},!1)}onGroupAdded(e){let t=e.serialize(),s=localStorage.getItem("nexus-socket-uuid");s&&(t.owner=s,e.configure(t)),e.onGroupMoved=this.onGroupMoved.bind(this,e),e.onGroupNodeMoved=this.onNodeMoved.bind(this),this.nexusSocket.sendMessage("workflow:update",{update:"group-added",data:t},!1)}onGroupRemoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"group-removed",data:t},!1)}onGroupMoved(e){let t=e.serialize();this.nexusSocket.sendMessage("workflow:update",{update:"group-moved",data:t},!1)}};var T=class extends EventTarget{constructor(e,t,s){super(),this.app=t,this.api=e,this.nexusSocket=s,this.comfy_events=["b_preview","progress","executing","executed","execution_start","execution_error","execution_cached"];let n=e.queuePrompt;this.apiWrapper={oqp:n,enableApi:!1,api:e,qp:async function(o,a){return this.enableApi?this.oqp.call(this.api,o,{output:a.output,workflow:a.workflow}):{node_errors:{}}}},this.api.queuePrompt=this.apiWrapper.qp.bind(this.apiWrapper),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.addEventListeners(),this.comfy_events.forEach(o=>{this.api.addEventListener(o,this.handleComfyEvents.bind(this))}),this.handleProgressDraw(),this.handleControl(!1)}handleProgressDraw(){let e=LGraphCanvas.prototype.drawNodeShape,t=this.app;LGraphCanvas.prototype.drawNodeShape=function(s,n,o,a,r,h,l){let d=e.apply(this,arguments),p=t.lastNodeErrors?.[s.id],u=null,m=1;if(s.id===t.runningNodeId?u="#0f0":t.dragOverNode&&s.id===t.dragOverNode.id?u="dodgerblue":p?.errors?(u="red",m=2):t.lastExecutionError&&t.lastExecutionError.node_id===s.id&&(u="#f0f",m=2),u){let f=s._shape||s.constructor.shape||LiteGraph.ROUND_SHAPE;n.lineWidth=m,n.globalAlpha=.8,n.beginPath(),f==LiteGraph.BOX_SHAPE?n.rect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT):f==LiteGraph.ROUND_SHAPE||f==LiteGraph.CARD_SHAPE&&s.flags.collapsed?n.roundRect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT,this.round_radius*2):f==LiteGraph.CARD_SHAPE?n.roundRect(-6,-6-LiteGraph.NODE_TITLE_HEIGHT,12+o[0]+1,12+o[1]+LiteGraph.NODE_TITLE_HEIGHT,[this.round_radius*2,this.round_radius*2,2,2]):f==LiteGraph.CIRCLE_SHAPE&&n.arc(o[0]*.5,o[1]*.5,o[0]*.5+6,0,Math.PI*2),n.strokeStyle=u,n.stroke(),n.strokeStyle=a,n.globalAlpha=1}if(t.progress&&s.id===t.runningNodeId&&(n.fillStyle="green",n.fillRect(0,0,o[0]*(t.progress.value/t.progress.max),6),n.fillStyle=r),p){n.lineWidth=2,n.strokeStyle="red";for(let f of p.errors)if(f.extra_info&&f.extra_info.input_name){let g=s.findInputSlot(f.extra_info.input_name);if(g!==-1){let b=s.getConnectionPos(!0,g);n.beginPath(),n.arc(b[0]-s.pos[0],b[1]-s.pos[1],12,0,2*Math.PI,!1),n.stroke()}}}return d}}handleMessageReceive(e){try{this.dispatchEvent(new CustomEvent(e.name,{detail:e}))}catch(t){console.error("Error handling message:",t)}}handleComfyEvents(e){e.type==="b_preview"?this.blobToBase64(e.detail).then(t=>{this.nexusSocket.sendMessage(e.type,{detail:t},!1)}):this.nexusSocket.sendMessage(e.type,{detail:e.detail},!1)}addEventListeners(){this.addEventListener("progress",({detail:e})=>{e=e.data.detail,this.app.progress=e,this.app.graph.setDirtyCanvas(!0,!1)}),this.addEventListener("executing",({detail:e})=>{e=e.data.detail,this.app.progress=null,this.app.runningNodeId=e,this.app.graph.setDirtyCanvas(!0,!1),delete this.app.nodePreviewImages[this.app.runningNodeId]}),this.addEventListener("executed",({detail:e})=>{e=e.data.detail;let t=this.app.nodeOutputs[e.node];if(e.merge&&t)for(let n in e.output??{}){let o=t[n];o instanceof Array?t[n]=o.concat(e.output[n]):t[n]=e.output[n]}else this.app.nodeOutputs[e.node]=e.output;let s=this.app.graph.getNodeById(e.node);s&&s.onExecuted&&s.onExecuted(e.output)}),this.addEventListener("execution_start",({detail:e})=>{e=e.data.detail,this.app.runningNodeId=null,this.app.lastExecutionError=null,this.app.graph._nodes.forEach(t=>{t.onExecutionStart&&t.onExecutionStart()})}),this.addEventListener("execution_error",({detail:e})=>{e=e.data.detail,this.app.lastExecutionError=e;let t=this.formatExecutionError(e);this.app.ui.dialog.show(t),this.app.canvas.draw(!0,!0)}),this.addEventListener("b_preview",({detail:e})=>{e=e.data.detail;let t=this.app.runningNodeId;if(t==null)return;let s=this.base64ToBlob(e),n=URL.createObjectURL(s);this.app.nodePreviewImages[t]=[n]})}blobToBase64(e){return new Promise((t,s)=>{let n=new FileReader;n.onloadend=()=>{t(n.result)},n.onerror=s,n.readAsDataURL(e)})}base64ToBlob(e){let[t,s]=e.split(","),n=t.match(/:(.*?);/)[1],o=atob(s),a=new Array(o.length);for(let h=0;h{e==="display"&&(this.defaultDisplay=t),this.el.style[e]=t})}removeStyles(){this.el&&(Object.keys(this.styles).forEach(e=>{this.el.style[e]=""}),this.styles={})}updateAttrs(e={}){this.attrs={...this.attrs,...e},this.addAttrs()}addAttrs(){this.el&&Object.entries(this.attrs).forEach(([e,t])=>{this.el.setAttribute(e,t)})}removeAttrs(){this.el&&(Object.keys(this.attrs).forEach(e=>{this.el.removeAttribute(e)}),this.attrs={})}addChildrens(){this.el&&(this.childrens.forEach(e=>{typeof e=="string"?this.el.insertAdjacentHTML("beforeend",e):e instanceof Node?this.el.appendChild(e):e instanceof w?this.el.appendChild(e.el):e instanceof i&&this.el.appendChild(e.el)}),this.afterChildrenAppend&&requestAnimationFrame(()=>this.afterChildrenAppend(this.childrens,this)))}addChild(e){this.el&&(typeof e=="string"?this.el.insertAdjacentHTML("beforeend",e):e instanceof Node?this.el.appendChild(e):e instanceof i&&this.el.appendChild(e.el),this.childrens.includes(e)||this.childrens.push(e),this.afterChildrenAppend&&this.afterChildrenAppend(this.childrens,this))}removeChildrens(){this.el&&(this.el.innerHTML="",this.childrens=[])}addEvents(){this.el&&Object.entries(this.events).forEach(([e,t])=>{this.el.addEventListener(e,s=>{t(s,this.parent)})})}removeEvents(){this.el&&(Object.entries(this.events).forEach(([e,t])=>{this.el.removeEventListener(e,t)}),this.events={})}show(){this.el&&(this.el.style.display=this.defaultDisplay,this.visible=!0)}hide(){this.el&&(this.el.style.display="none",this.visible=!1)}addStateStyles(){this.el&&(this.el.addEventListener("mouseenter",()=>this.applyStyles(this.hoverStyles)),this.el.addEventListener("mouseleave",()=>this.applyStyles(this.styles)),this.el.addEventListener("focus",()=>this.applyStyles(this.focusStyles),!0),this.el.addEventListener("blur",()=>this.applyStyles(this.styles),!0),this.el.addEventListener("mousedown",()=>this.applyStyles(this.activeStyles)),this.el.addEventListener("mouseup",()=>this.applyStyles(this.styles)))}applyStyles(e){this.visible&&Object.entries(e).forEach(([t,s])=>{this.el.style[t]=s})}};var z=new c({id:"nexus-chat-main",tag:"div",visible:!0,styles:{width:"512px",minHeight:"170px",maxHeight:"170px",overflow:"hidden",padding:"16px",borderRadius:"8px",border:"1px solid rgba(94,94,94,0.5)"},hoverStyles:{overflowY:"scroll"},afterChildrenAppend:function(i,e){i.length>50&&(i[0].removeElement(),i.shift()),e.el.scrollTop=e.el.scrollHeight}}),_=new c({id:"nexus-input-main",tag:"div",visible:!0,styles:{width:"100%",height:"32px",display:"flex",alignItems:"center"},childrens:[new c({id:"nexus-input-label",tag:"span",visible:!1,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',display:"block",fontSize:"14px",lineHeight:"14px",fontWeight:600,color:"white",padding:"0",margin:"0",textShadow:"1pt 1pt #000"},childrens:["Say:"]}),new c({id:"nexus-input",tag:"input",visible:!1,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:"14px",lineHeight:"14px",fontWeight:600,width:"100%",color:"white",padding:"0",margin:"0",border:"none",background:"transparent",flexGrow:1,outline:"none",textIndent:"4pt",caretColor:"transparent",textShadow:"1pt 1pt #000"}})]}),C=new c({id:"nexus-chat-window",tag:"div",styles:{position:"absolute",left:"16px",top:"100px",display:"flex",flexDirection:"column",zIndex:1e3},childrens:[new c({id:"nexus-total-players",tag:"div",visible:!0,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontSize:"14px",lineHeight:"14px",fontWeight:600,left:"16px",top:"80px",color:"#a5a5a5",width:"100%",paddingBottom:"12px",textAlign:"right"},childrens:["Users Online: ",new c({id:"nexus-total-players-count",visible:!0,tag:"span",childrens:["0"]})]}),z,_,new w({styles:{"#nexus-chat-main::-webkit-scrollbar":{display:"none","scrollbar-width":"none","-ms-overflow-style":"none"},"#nexus-chat-window":{}}})]}),v=function(i,e,t,s,n){let o="";n?o=`${i} ${e}`:o=`${i}: ${e}`;let a=new c({tag:"span",visible:!0,styles:{fontFamily:'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',display:"block",fontSize:"14px",lineHeight:"14px",fontWeight:600,width:"100%",color:"white",padding:"0",margin:"0",textShadow:"1pt 1pt #000",marginBottom:"1pt",lineBreak:"anywhere"},childrens:[o]});z.addChild(a)};var I=class{constructor(){this.active=0,this.panel=new c({id:"nexus-player-panel",tag:"div",visible:!1,styles:{width:"100%",height:"100vh",display:"flex","align-items":"center","justify-content":"center","box-sizing":"border-box",position:"absolute",left:"0",top:"0","z-index":"10000"},childrens:[new c({id:"nexus-player-panel-table",tag:"div",visible:!0,styles:{width:"832px",display:"flex","flex-direction":"column","border-radius":"6px","box-sizing":"border-box"},childrens:[new c({id:"nexus-player-panel-table-body",tag:"div",visible:!0,styles:{width:"100%",display:"flex","flex-direction":"column","align-items":"center","justify-content":"flex-start",color:"#a3a3a3","font-family":"Calibri","font-weight":"600",gap:"12px","padding-bottom":"12px","box-sizing":"border-box",background:"rgba(54,54,54,0.8)","border-radius":"6px","box-shadow":"4px 8px 8px rgba(0,0,0,0.5)","max-height":"540px",overflowY:"auto"}}),new w({styles:{"#nexus-player-panel .table-row":{width:"100%",display:"grid","align-items":"center","grid-template-columns":"20% 60% 20%","justify-content":"space-between"},"#nexus-player-panel .table-row:first-child":{width:"100%",color:"#a3a3a3",padding:"16px 0",background:"rgb(0,0,0,0.6)","border-top-left-radius":"6px","border-top-right-radius":"6px",position:"sticky",top:"0"},"#nexus-player-panel .table-row .table-row-child":{display:"flex","justify-content":"flex-start",gap:"12px","padding-left":"12px",width:"100%"},"#nexus-player-panel .table-row-child button":{border:"none",padding:"6px 12px","border-radius":"3px",cursor:"pointer",background:"rgba(94,94,94)",color:"#a1a1a1"},"#nexus-player-panel .table-row-child button:active":{opacity:"0.8"},"#nexus-player-panel .table-row-child button[active='1'][label='spectate']":{background:"#218c74",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='editor']":{background:"#474787",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='mouse']":{background:"#cd6133",color:"#fafafa"},"#nexus-player-panel .table-row-child button[active='1'][label='queue']":{background:"#6F1E51",color:"#fafafa"}}})]})]}),this.onCommandReceived=null,this.buttons=[],this.columns=[]}addColumnsToPanel(e){this.columns=e}addRowsToPanel(e,t,s,n){let o=this.panel.childrens[0].childrens[0];o.removeChildrens(),this.buttons=[];let a=document.createElement("div");a.className="table-row",this.columns.forEach(r=>{let h=document.createElement("span");h.className="table-row-child",h.textContent=r,a.appendChild(h)}),o.addChild(a),e.forEach((r,h)=>{let l=document.createElement("div");l.className="table-row",r.forEach((d,p)=>{let u=document.createElement("span");u.className="table-row-child";let m=d.indexOf(",")>-1,f=d.indexOf(":")>-1,g=[];m?(g=d.split(","),g.forEach((b,k)=>{let S=b.split(":");this.addChildToParent(S,u,t,s,n,h,p,k)})):f?(g=d.split(":"),this.addChildToParent(g,u,t,s,n,h,p,-1)):u.textContent=d,l.appendChild(u)}),o.addChild(l)})}addChildToParent(e,t,s,n,o,a,r,h){switch(e[0]){case"button":let l=document.createElement("button");if(e[1]=="none"&&(l.className="none"),this.buttons.includes(l)||this.buttons.push(l),l.textContent=e[1],s[a]&&s[a][r]&&n[a][r]&&o[a][r]){let d=n[a][r].split(","),p=o[a][r].split(",");h!=-1?(l.setAttribute("data",p[h]),l.setAttribute("active",d[h])):(l.setAttribute("data",o[a][r]),l.setAttribute("active",n[a][r])),l.setAttribute("kind",e[0]),l.setAttribute("label",e[1]),e[1]!="none"&&(l.onclick=s[a][r].bind(this))}t.appendChild(l);break}}disableButtons(e){this.buttons.forEach(t=>{let s=t.getAttribute("kind"),n=t.getAttribute("data"),o=parseInt(t.getAttribute("active")),a=t.getAttribute("label");a==e&&o==1&&(t.setAttribute("active",0),this.onCommandReceived&&this.onCommandReceived(s,a,n,0))})}onclick(e){let t=e.target.getAttribute("kind"),s=e.target.getAttribute("data"),n=e.target.getAttribute("label"),o=parseInt(e.target.getAttribute("active"));t=="button"&&(n=="spectate"&&this.disableButtons(n),o==0?e.target.setAttribute("active",1):o==1&&e.target.setAttribute("active",0)),o=parseInt(e.target.getAttribute("active")),this.onCommandReceived&&this.onCommandReceived(t,n,s,o)}};var P=class{constructor(){this.active=0,this.panel=new c({id:"nexus-workflow-panel",tag:"div",visible:!1,styles:{width:"100%",height:"100vh",display:"flex","align-items":"center","justify-content":"center","box-sizing":"border-box",position:"absolute",left:"0",top:"0","z-index":"10000"},childrens:[new c({id:"nexus-workflow-panel-table",tag:"div",visible:!0,styles:{width:"512px",display:"flex","flex-direction":"column","border-radius":"6px","box-sizing":"border-box"},childrens:[new c({id:"nexus-workflow-panel-table-body",tag:"div",visible:!0,styles:{width:"100%",display:"flex","flex-direction":"column","align-items":"center","justify-content":"flex-start",color:"#a3a3a3","font-family":"Calibri","font-weight":"600",gap:"12px","padding-bottom":"12px","box-sizing":"border-box",background:"rgba(54,54,54,0.8)","border-radius":"6px","box-shadow":"4px 8px 8px rgba(0,0,0,0.5)","max-height":"540px",overflowY:"auto"}}),new w({styles:{"#nexus-workflow-panel .table-row":{width:"100%",display:"flex","align-items":"center","justify-content":"center"},"#nexus-workflow-panel .table-row:first-child":{width:"100%",color:"#a3a3a3",padding:"16px 0",background:"rgb(0,0,0,0.6)","border-top-left-radius":"6px","border-top-right-radius":"6px",position:"sticky",top:"0"},"#nexus-workflow-panel .table-row .table-row-child":{display:"flex","justify-content":"flex-start",gap:"12px","padding-left":"12px",width:"100%"},"#nexus-workflow-panel .table-row-child button":{border:"none",padding:"6px 12px","border-radius":"3px",cursor:"pointer",background:"rgba(94,94,94)",color:"#a1a1a1",width:"100%"},"#nexus-workflow-panel .table-row-child:first-child":{width:"50%"},"#nexus-workflow-panel .table-row-child:last-child":{"padding-right":"12px"},"#nexus-workflow-panel .table-row-child button:active":{opacity:"0.8"},"#nexus-workflow-panel .table-row-child button[label='load']:active":{background:"#218c74",color:"#fafafa"},"#nexus-workflow-panel .table-row-child button[label='loadforall']:active":{background:"#474787",color:"#fafafa"}}})]})]}),this.onCommandReceived=null,this.buttons=[],this.columns=[]}addColumnsToPanel(e){this.columns=e}addRowsToPanel(e,t,s,n){let o=this.panel.childrens[0].childrens[0];o.removeChildrens(),this.buttons=[];let a=document.createElement("div");a.className="table-row",this.columns.forEach(r=>{let h=document.createElement("span");h.className="table-row-child",h.textContent=r,a.appendChild(h)}),o.addChild(a),e.forEach((r,h)=>{let l=document.createElement("div");l.className="table-row",r.forEach((d,p)=>{let u=document.createElement("span");u.className="table-row-child";let m=d.indexOf(",")>-1,f=d.indexOf(":")>-1,g=[];m?(g=d.split(","),g.forEach((b,k)=>{let S=b.split(":");this.addChildToParent(S,u,t,s,n,h,p,k)})):f?(g=d.split(":"),this.addChildToParent(g,u,t,s,n,h,p,-1)):(d=d.split("|"),d=d.join(":"),u.textContent=d),l.appendChild(u)}),o.addChild(l)})}addChildToParent(e,t,s,n,o,a,r,h){switch(e[0]){case"button":let l=document.createElement("button");if(e[1]=="none"&&(l.className="none"),this.buttons.includes(l)||this.buttons.push(l),l.textContent=e[1],s[a]&&s[a][r]&&n[a][r]&&o[a][r]){let d=n[a][r].split(","),p=o[a][r].split(",");h!=-1?(l.setAttribute("data",p[h]),l.setAttribute("active",d[h])):(l.setAttribute("data",o[a][r]),l.setAttribute("active",n[a][r])),l.setAttribute("kind",e[0]),l.setAttribute("label",e[1]),e[1]!="none"&&(l.onclick=s[a][r].bind(this))}t.appendChild(l);break}}onclick(e){let t=e.target.getAttribute("kind"),s=e.target.getAttribute("data"),n=e.target.getAttribute("label"),o=parseInt(e.target.getAttribute("active"));t=="button"&&(o==0?e.target.setAttribute("active",1):o==1&&e.target.setAttribute("active",0)),o=parseInt(e.target.getAttribute("active")),this.onCommandReceived&&this.onCommandReceived(t,n,s,o)}};var R=class{constructor(e,t,s,n,o){this.color=t,this.colors={join:"#ff6e4d",info:"#fba510",personal:"#fba510",name:"#00fa00",none:"#ffffff"},this.inputMain=_,this.nexusSocket=e,this.workflowManager=n,this.promptControl=o,this.mouseManger=s,this.nexusSocket.onConnected.push(this.handleConnect.bind(this)),this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this)),this.inputMain.addEventListener("message",this.handleMessage.bind(this)),document.addEventListener("keydown",this.handleChatWindow.bind(this)),C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","none"),C.show(),setInterval(a=>{a.childrens[0].childrens[1].el.textContent=Object.keys(this.nexusSocket.userManager.users).length},1e3,C),this.up=new I,this.up.onCommandReceived=this.onPanelCommandReceived.bind(this),this.wp=new P,this.wp.onCommandReceived=this.onWFPanelCommandReceived.bind(this),this.spectating=null}async verifyLogin(){let e=localStorage.getItem("nexus-socket-token");if(e){let t=await B(e);t&&this.nexusSocket.uuid==t.user_id?(this.promptControl.handleControl(!0),this.promptControl.handleEditor(!0),this.workflowManager.handleWorkflowEvents(!0),this.nexusSocket.admin=!0,v("","You are now an admin!",this.color.info,this.colors.personal,!0),this.workflowManager.saveManager(60)):(this.promptControl.handleControl(!1),this.promptControl.handleEditor(!1),this.workflowManager.handleWorkflowEvents(!1),this.workflowManager.removeSaveManager(),v("","You are a viewer!",this.color.info,this.colors.info,!0))}else{let t=await O(this.nexusSocket.uuid);if(t){let s=t.editor,n=t.queue;this.workflowManager.handleWorkflowEvents(s),this.promptControl.handleEditor(s),this.promptControl.handleControl(n),s?(this.workflowManager.saveManager(60),this.nexusSocket.editor=!0):(this.workflowManager.handleWorkflowEvents(!1),this.workflowManager.removeSaveManager()),v("",`Permission: Workflow Editor ${s?"Yes":"No"} | Queue Prompt ${n?"Yes":"No"} `,this.color.info,this.colors.personal,!0)}}this.workflowManager.loadLatestWorkflow()}handleConnect(){let e=this.nexusSocket.userManager.get(this.nexusSocket.uuid,"name");this.nexusSocket.sendMessage("chat-join",{name:e},!1),this.verifyLogin()}handleChatWindow(e){let t=e.key,s=_.childrens[0],n=_.childrens[1],o={tag:document.activeElement.tagName,el:document.activeElement,chat:document.activeElement==n.el};function a(){!["INPUT","TEXTAREA"].includes(o.tag)&&n.visible==!1&&(e.preventDefault(),s.show(),n.show(),n.el.focus(),n.el.value="",n.focusTimer=setInterval((d,p)=>{document.activeElement!=p&&d.visible==!0&&p.focus()},500,n,n.el),C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","all"))}function r(){o.chat&&(s.hide(),n.el.blur(),n.el.value="",n.hide(),clearTimeout(n.focusTimer),C.childrens[3].softUpdate("#nexus-chat-window","pointer-events","none"))}function h(){if(!o.chat)return;let l=n.el.value;_.dispatchEvent(new CustomEvent("message",{detail:{message:l}})),r()}switch(t){case"t":case"T":a(),this.togglePlayerTab(!0),this.toggleWFTab(!0);break;case"Escape":r();break;case"Enter":h();break;case"p":case"P":e.altKey&&(o.chat||this.togglePlayerTab());break;case"o":case"O":e.altKey&&(o.chat||this.toggleWFTab());break}}toggleWFTab(e,t){(this.nexusSocket.admin||this.nexusSocket.editor)&&(e?(this.wp.active=0,this.wp.panel.hide()):t?this.prepareWFPanel():this.wp.active==0?(this.wp.active=1,this.wp.panel.show(),this.prepareWFPanel()):this.wp.active==1&&(this.wp.active=0,this.wp.panel.hide()),this.wp.active==1?this.workflowManager.removeSaveManager():this.workflowManager.saveManager(this.workflowManager.saveTime||60))}togglePlayerTab(e,t){e?(this.up.active=0,this.up.panel.hide()):t?this.preparePanel(this.nexusSocket.admin):this.up.active==0?(this.up.active=1,this.up.panel.show(),this.preparePanel(this.nexusSocket.admin)):this.up.active==1&&(this.up.active=0,this.up.panel.hide())}async onWFPanelCommandReceived(e,t,s,n){switch(e){case"button":if(t=="load"){let o=s,a=await F(this.nexusSocket.uuid,o);if(a){a.clear=!0;let r={name:"workflow",from:this.nexusSocket.uuid,data:{workflow:a}};this.nexusSocket.onMessageReceive.forEach(h=>{h(r)}),this.nexusSocket.admin&&this.nexusSocket.sendMessage("workflow",{workflow:a},!1)}}break}}async onPanelCommandReceived(e,t,s,n){switch(e){case"button":if(t=="spectate")this.nexusSocket.sendMessage("spectate",{on:n,receiver:s},!1),n==1?this.spectating=s:this.spectating=null;else if(t=="editor"){if(this.nexusSocket.admin){let o=localStorage.getItem("nexus-socket-token");await G(s,this.nexusSocket.uuid,{editor:!!n},o),this.nexusSocket.sendMessage("permission",{receiver:s},!1)}}else if(t=="queue"){if(this.nexusSocket.admin){let o=localStorage.getItem("nexus-socket-token");await G(s,this.nexusSocket.uuid,{queue:!!n},o),this.nexusSocket.sendMessage("permission",{receiver:s},!1)}}else t=="mouse"&&(this.nexusSocket.sendMessage("mouse_view_toggle",{on:n,receiver:s},!1),this.nexusSocket.userManager.set(s,"hide_mouse",n));break}}async prepareWFPanel(){let e=["Name","Updated","Action"];this.wp.addColumnsToPanel(e);let t=await $(this.nexusSocket.uuid),s=[],n=[],o=[],a=[];for(let r=0;r]+(>|$)/g;e=e.replace(a,"");for(let r of e)n.test(r)&&!o.test(r)||(s+=r);return s}}startsWithCommand(e){return/^\/\w+(\s+\S+)?/.test(e)}extractCommandAndValue(e){let t=/^\/(\w+)\s*(.*)/,s=e.match(t);if(s){let n=s[1],o=s[2].trim()?s[2].split(/\s+/):[];return{command:n,values:o}}else return null}};var j=window.comfyAPI.app.app,q=window.comfyAPI.api.api,M=new N,J={name:"ComfyUI-Nexus",async setup(i){let e=new L(M,i,60),t=new A(i,q,M),s=new T(q,i,M);new R(M,e.color,e,t,s),M.connect()},async afterConfigureGraph(){console.log(j)}};j.registerExtension(J);})();