├── web
├── assets
│ └── img
│ │ └── logo.png
├── lib
│ ├── nexus.user.manager.js
│ ├── nexus.styles.js
│ ├── nexus.api.js
│ ├── nexus.sockets.js
│ ├── nexus.chat.js
│ ├── nexus.elements.js
│ ├── nexus.mouse.js
│ ├── nexus.workflow.panel.js
│ ├── nexus.comfy.js
│ ├── nexus.panel.js
│ ├── nexus.workflow.js
│ ├── nexus.litegraph.js
│ └── nexus.cmd.js
└── util.js
├── classes
└── WebSocketManager.py
├── pyproject.toml
├── .github
└── workflows
│ └── publish.yml
├── LICENSE
├── .gitignore
├── README.md
└── __init__.py
/web/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daxcay/ComfyUI-Nexus/HEAD/web/assets/img/logo.png
--------------------------------------------------------------------------------
/classes/WebSocketManager.py:
--------------------------------------------------------------------------------
1 | class WebSocketManager:
2 | def __init__(self):
3 | self.sockets = {}
4 |
5 | def get(self, id):
6 | return self.sockets.get(id)
7 |
8 | def set(self, id, ws):
9 | self.sockets[id] = ws
10 |
11 | def delete(self, id):
12 | if id in self.sockets:
13 | del self.sockets[id]
14 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "comfyui-nexus"
3 | description = "Node to enable seamless multiuser workflow collaboration"
4 | version = "1.0.2"
5 | license = { file = "LICENSE" }
6 | dependencies = []
7 |
8 | [project.urls]
9 | Repository = "https://github.com/daxcay/ComfyUI-Nexus"
10 | # Used by Comfy Registry https://comfyregistry.org
11 |
12 | [tool.comfy]
13 | PublisherId = "daxcay"
14 | DisplayName = "ComfyUI-Nexus"
15 | Icon = "https://raw.githubusercontent.com/daxcay/ComfyUI-Nexus/master/web/assets/img/logo.png"
16 |
--------------------------------------------------------------------------------
/web/lib/nexus.user.manager.js:
--------------------------------------------------------------------------------
1 | export class UserManager {
2 | constructor() {
3 | this.users = {}
4 | }
5 |
6 | create(id) {
7 | if (!this.users[id]) {
8 | this.users[id] = {}
9 | }
10 | }
11 |
12 | set(id, key, data) {
13 | this.create(id)
14 | this.users[id][key] = data
15 | }
16 |
17 | get(id, key) {
18 | this.create(id)
19 | return this.users[id][key]
20 | }
21 |
22 | remove(id) {
23 | if(this.users[id])
24 | delete this.users[id];
25 | return true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Comfy registry
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "pyproject.toml"
9 |
10 | jobs:
11 | publish-node:
12 | name: Publish Custom Node to registry
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out code
16 | uses: actions/checkout@v4
17 | - name: Publish Custom Node
18 | uses: Comfy-Org/publish-node-action@main
19 | with:
20 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
21 |
--------------------------------------------------------------------------------
/web/util.js:
--------------------------------------------------------------------------------
1 | import { NexusSocket } from "./lib/nexus.sockets.js";
2 | import { NexusMouseManager } from "./lib/nexus.mouse.js";
3 | import { NexusWorkflowManager } from "./lib/nexus.workflow.js";
4 | import { NexusPromptControl } from "./lib/nexus.comfy.js";
5 | import { NexusCommands } from "./lib/nexus.cmd.js";
6 |
7 | let app = window.comfyAPI.app.app;
8 | let api = window.comfyAPI.api.api;
9 | let nexusSocket = new NexusSocket()
10 |
11 | let nexus = {
12 | name: "ComfyUI-Nexus",
13 | async setup(app) {
14 | app = app
15 | let mouseManager = new NexusMouseManager(nexusSocket, app, 10)
16 | let workflowManager = new NexusWorkflowManager(app, api, nexusSocket)
17 | let nexusPromptControl = new NexusPromptControl(api, app, nexusSocket)
18 | new NexusCommands(app, api, nexusSocket, mouseManager.color, mouseManager, workflowManager, nexusPromptControl)
19 | nexusSocket.connect()
20 | },
21 | async beforeConfigureGraph() {
22 | }
23 | }
24 |
25 | app.registerExtension(nexus);
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 daxcay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/web/lib/nexus.styles.js:
--------------------------------------------------------------------------------
1 | export class StyleInjector {
2 |
3 | constructor({ styles }) {
4 | this.el = null
5 | this.styles = styles;
6 | this.create()
7 | }
8 |
9 | create() {
10 | this.el = document.createElement('style');
11 | let cssString = "";
12 | for (const selector in this.styles) {
13 | cssString += `${selector} { `;
14 | for (const property in this.styles[selector]) {
15 | cssString += `${property}: ${this.styles[selector][property]}; `;
16 | }
17 | cssString += `} `;
18 | }
19 | this.el.textContent = cssString;
20 | }
21 |
22 | softUpdate(selector, property, value) {
23 | let cssString = "";
24 | for (const key in this.styles) {
25 | cssString += `${key} { `;
26 | for (const property in this.styles[key]) {
27 | cssString += `${property}: ${this.styles[key][property]}; `;
28 | }
29 | if(key == selector) {
30 | cssString += `${property}: ${value}; `;
31 | }
32 | cssString += `} `;
33 | }
34 | this.el.textContent = cssString;
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *.pyc
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | # lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
153 | *.pyc
154 |
155 | .idea
156 | /node_modules
157 | /frontend
--------------------------------------------------------------------------------
/web/lib/nexus.api.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = 'nexus';
2 |
3 | async function fetchWithHandling(url, options = {}) {
4 | try {
5 | // console.info("Calling: ", url)
6 | const response = await fetch(url, options);
7 | const data = await response.json();
8 | if (!response.ok) {
9 | throw new Error(data.error || 'Something went wrong');
10 | }
11 | return data;
12 | } catch (error) {
13 | console.error(`Fetch error: ${error.message}`);
14 | return { error: error.message };
15 | }
16 | }
17 |
18 | export async function getUserSpecificWorkflows(userId) {
19 | const url = `${BASE_URL}/workflows`;
20 | const options = {
21 | method: 'POST',
22 | headers: { 'Content-Type': 'application/json' },
23 | body: JSON.stringify({ user_id: userId }),
24 | };
25 | return await fetchWithHandling(url, options);
26 | }
27 |
28 | export async function getUserSpecificWorkflow(userId, workflowId) {
29 | const url = `${BASE_URL}/workflows/${workflowId}`;
30 | const options = {
31 | method: 'POST',
32 | headers: { 'Content-Type': 'application/json' },
33 | body: JSON.stringify({ user_id: userId }),
34 | };
35 | return await fetchWithHandling(url, options);
36 | }
37 |
38 | export async function getUserSpecificWorkflowsMeta(userId) {
39 | const url = `${BASE_URL}/workflows/meta`;
40 | const options = {
41 | method: 'POST',
42 | headers: { 'Content-Type': 'application/json' },
43 | body: JSON.stringify({ user_id: userId }),
44 | };
45 | return await fetchWithHandling(url, options);
46 | }
47 |
48 | export async function postWorkflow(userId, workflowData) {
49 | const url = `${BASE_URL}/workflow`;
50 | const options = {
51 | method: 'POST',
52 | headers: { 'Content-Type': 'application/json' },
53 | body: JSON.stringify({ user_id: userId, ...workflowData }),
54 | };
55 | return await fetchWithHandling(url, options);
56 | }
57 |
58 | export async function postBackupWorkflow(userId, workflowData) {
59 | const url = `${BASE_URL}/workflow/backup`;
60 | const options = {
61 | method: 'POST',
62 | headers: { 'Content-Type': 'application/json' },
63 | body: JSON.stringify({ user_id: userId, ...workflowData }),
64 | };
65 | return await fetchWithHandling(url, options);
66 | }
67 |
68 | export async function getLatestWorkflow() {
69 | const url = `${BASE_URL}/workflow`;
70 | return await fetchWithHandling(url);
71 | }
72 |
73 | export async function getPermissionById(userId) {
74 | const url = `${BASE_URL}/permission/${userId}`;
75 | return await fetchWithHandling(url);
76 | }
77 |
78 | export async function postPermissionById(userId, adminId, data, token) {
79 | const url = `${BASE_URL}/permission/${userId}`;
80 | const options = {
81 | method: 'POST',
82 | headers: {
83 | 'Content-Type': 'application/json',
84 | 'Authorization': token,
85 | },
86 | body: JSON.stringify({ admin_id: adminId, data }),
87 | };
88 | return await fetchWithHandling(url, options);
89 | }
90 |
91 | export async function nexusLogin(uuid, account, password) {
92 | const url = `${BASE_URL}/login`;
93 | const options = {
94 | method: 'POST',
95 | headers: { 'Content-Type': 'application/json' },
96 | body: JSON.stringify({ uuid, account, password }),
97 | };
98 | return await fetchWithHandling(url, options);
99 | }
100 |
101 | export async function nexusVerify(token) {
102 | const url = `${BASE_URL}/verify`;
103 | const options = {
104 | method: 'POST',
105 | headers: { 'Content-Type': 'application/json' },
106 | body: JSON.stringify({ token }),
107 | };
108 | return await fetchWithHandling(url, options);
109 | }
110 |
111 | export async function getNameById(userId) {
112 | const url = `${BASE_URL}/name/${userId}`;
113 | return await fetchWithHandling(url);
114 | }
115 |
116 |
--------------------------------------------------------------------------------
/web/lib/nexus.sockets.js:
--------------------------------------------------------------------------------
1 | import { UserManager } from "./nexus.user.manager.js";
2 |
3 | export class NexusSocket extends EventTarget {
4 |
5 | getName() {
6 | let name = localStorage.getItem('nexus-socket-name');
7 | if (!name) {
8 | localStorage.setItem('nexus-socket-name', "User");
9 | name = "User"
10 | }
11 | this.userManager.set(this.uuid, "name", name)
12 | return name
13 | }
14 |
15 | getUUID() {
16 | let uuid = localStorage.getItem('nexus-socket-uuid');
17 | if (!uuid) {
18 | uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c =>
19 | (Math.random() * 16 | 0).toString(16)
20 | );
21 | localStorage.setItem('nexus-socket-uuid', uuid);
22 | }
23 | return uuid;
24 | }
25 |
26 | constructor() {
27 |
28 | super();
29 | this.ws = null;
30 | this.userManager = new UserManager();
31 | this.uuid = this.getUUID();
32 | this.admin = false
33 | this.wsUrl = `ws${window.location.protocol === "https:" ? "s" : ""}://${location.host}/nexus?id=${this.uuid}`;
34 |
35 | this.onConnected = [this.onConnectedHandler.bind(this)];
36 | this.onDisconnected = [];
37 | this.onMessageReceive = [this.onMessageReceiveHandler.bind(this)];
38 | this.onMessageSend = [];
39 | this.onError = [];
40 | this.onClose = [];
41 |
42 | this.getName()
43 |
44 | }
45 |
46 | onConnectedHandler() {
47 | this.userManager.create(this.uuid);
48 | this.sendMessage('join', { name: this.getName() }, false);
49 | this.sendMessage("online", {}, false)
50 | }
51 |
52 | onMessageReceiveHandler(message) {
53 | let name = message.name;
54 | let from = message.from;
55 | switch (name) {
56 | case "join":
57 | this.userManager.create(from);
58 | this.sendMessage('hi', {}, false);
59 | break;
60 | case "hi":
61 | this.userManager.create(from);
62 | break;
63 | }
64 | }
65 |
66 | connect() {
67 | try {
68 | this.ws = new WebSocket(this.wsUrl);
69 | this.ws.onopen = () => {
70 | if (this.onConnected) {
71 | this.onConnected.forEach(f => { f(); });
72 | }
73 | };
74 | this.ws.onmessage = (msg) => {
75 | try {
76 | const message = JSON.parse(msg.data);
77 | if (this.onMessageReceive) {
78 | this.onMessageReceive.forEach(f => { f(message); });
79 | }
80 | } catch (e) {
81 | console.error('Error handling message:', e);
82 | }
83 | };
84 | this.ws.onerror = (error) => {
85 | if (this.onError) {
86 | this.onError.forEach(f => { f(error); });
87 | }
88 | console.error('WebSocket error:', error);
89 | };
90 | this.ws.onclose = () => {
91 | if (this.onDisconnected) {
92 | this.onDisconnected.forEach(f => { f(); });
93 | }
94 | setTimeout(() => {
95 | this.connect();
96 | }, 5000);
97 | };
98 | } catch (e) {
99 | console.error('Error connecting to WebSocket:', e);
100 | }
101 | }
102 |
103 | closeWebSocket() {
104 | if (this.ws && this.ws.readyState === WebSocket.OPEN) {
105 | this.ws.close();
106 | } else {
107 | console.log('WebSocket is not open.');
108 | }
109 | }
110 |
111 | sendMessage(event_name, event_data, all = true) {
112 | try {
113 | if (this.ws && this.ws.readyState === WebSocket.OPEN) {
114 | let data = {}
115 | if(event_data.receiver) {
116 | data.receiver = event_data.receiver
117 | delete event_data.receiver
118 | } else if (event_data.exclude) {
119 | data.exclude = event_data.exclude
120 | delete event_data.exclude
121 | }
122 | data.from = this.uuid
123 | data.name = event_name
124 | data.data = event_data
125 | data.all = all
126 | this.ws.send(JSON.stringify(data));
127 | if (this.onMessageSend) {
128 | this.onMessageSend.forEach(f => { f(message); });
129 | }
130 | }
131 | } catch (e) {
132 | console.error(`Error sending ${event_name} message:`, e);
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/web/lib/nexus.chat.js:
--------------------------------------------------------------------------------
1 | import { DynamicElement } from "./nexus.elements.js"
2 | import { StyleInjector } from "./nexus.styles.js";
3 |
4 | let chatMain = new DynamicElement({
5 | id: 'nexus-chat-main',
6 | tag: 'div',
7 | visible: true,
8 | styles: {
9 | width: '512px',
10 | minHeight: '170px',
11 | maxHeight: '170px',
12 | overflow: 'hidden',
13 | padding: '16px',
14 | borderRadius: '8px',
15 | border: '1px solid rgba(94,94,94,0.5)'
16 | },
17 | hoverStyles: {
18 | overflowY: 'scroll'
19 | },
20 | afterChildrenAppend: function (childrens, me) {
21 | if (childrens.length > 50) {
22 | childrens[0].removeElement()
23 | childrens.shift()
24 | }
25 | me.el.scrollTop = me.el.scrollHeight
26 | }
27 | })
28 |
29 | let inputMain = new DynamicElement({
30 | id: 'nexus-input-main',
31 | tag: 'div',
32 | visible: true,
33 | styles: {
34 | width: '100%',
35 | height: '32px',
36 | display: 'flex',
37 | alignItems: 'center',
38 | },
39 | childrens: [
40 | new DynamicElement({
41 | id: 'nexus-input-label',
42 | tag: 'span',
43 | visible: false,
44 | styles: {
45 | fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
46 | display: 'block',
47 | fontSize: '14px',
48 | lineHeight: '14px',
49 | fontWeight: 600,
50 | color: 'white',
51 | padding: '0',
52 | margin: '0',
53 | textShadow: '1pt 1pt #000',
54 | },
55 | childrens: ['Say:']
56 | }),
57 | new DynamicElement({
58 | id: 'nexus-input',
59 | tag: 'input',
60 | visible: false,
61 | styles: {
62 | fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
63 | fontSize: '14px',
64 | lineHeight: '14px',
65 | fontWeight: 600,
66 | width: '100%',
67 | color: 'white',
68 | padding: '0',
69 | margin: '0',
70 | border: 'none',
71 | background: "transparent",
72 | flexGrow: 1,
73 | outline: 'none',
74 | textIndent: '4pt',
75 | caretColor: 'transparent',
76 | textShadow: '1pt 1pt #000',
77 | }
78 | })
79 | ]
80 | })
81 |
82 | let chatWindow = new DynamicElement({
83 | id: 'nexus-chat-window',
84 | tag: 'div',
85 | styles: {
86 | position: 'absolute',
87 | left: "16px",
88 | top: '100px',
89 | display: 'flex',
90 | flexDirection: 'column',
91 | zIndex: 1000
92 | },
93 | childrens: [
94 | new DynamicElement({
95 | id: "nexus-total-players",
96 | tag: "div",
97 | visible: true,
98 | styles: {
99 | fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
100 | fontSize: '14px',
101 | lineHeight: '14px',
102 | fontWeight: 600,
103 | left: "16px",
104 | top: '80px',
105 | color: "#a5a5a5",
106 | width: "100%",
107 | paddingBottom: "12px",
108 | textAlign: "right"
109 | },
110 | childrens: ['Users Online: ', new DynamicElement({ id: "nexus-total-players-count", visible: true, tag: "span", childrens:['0'] })]
111 | }),
112 | chatMain,
113 | inputMain,
114 | new StyleInjector({
115 | styles: {
116 | '#nexus-chat-main::-webkit-scrollbar': {
117 | 'display': 'none',
118 | 'scrollbar-width': 'none',
119 | '-ms-overflow-style': 'none',
120 | },
121 | "#nexus-chat-window" : {
122 | // for pointer events
123 | }
124 | }
125 | })
126 | ]
127 | })
128 |
129 | let addChatWindowMessage = function (subject, message, subject_color, message_color, merge) {
130 | let msg = ""
131 | if (merge) {
132 | msg = `${subject} ${message}`
133 | }
134 | else {
135 | msg = `${subject}: ${message}`
136 | }
137 |
138 | let chatElement = new DynamicElement({
139 | tag: 'span',
140 | visible: true,
141 | styles: {
142 | fontFamily: 'Calibri, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
143 | display: 'block',
144 | fontSize: '14px',
145 | lineHeight: '14px',
146 | fontWeight: 600,
147 | width: '100%',
148 | color: 'white',
149 | padding: '0',
150 | margin: '0',
151 | textShadow: '1pt 1pt #000',
152 | marginBottom: '1pt',
153 | lineBreak: 'anywhere'
154 | },
155 | childrens: [msg],
156 | })
157 | chatMain.addChild(chatElement)
158 | }
159 |
160 | export { chatMain, inputMain, chatWindow, addChatWindowMessage }
--------------------------------------------------------------------------------
/web/lib/nexus.elements.js:
--------------------------------------------------------------------------------
1 | import { StyleInjector } from "./nexus.styles.js";
2 |
3 | export class DynamicElement extends EventTarget {
4 | id = '';
5 | tag = '';
6 | styles = {};
7 | hoverStyles = {};
8 | focusStyles = {};
9 | activeStyles = {};
10 | attrs = {};
11 | childrens = [];
12 | events = {};
13 | appendTo = document.body;
14 | visible = false;
15 | afterChildrenAppend = null;
16 | afterCreated = null;
17 | defaultDisplay = 'initial';
18 | el = null;
19 |
20 | constructor({
21 | id,
22 | tag,
23 | styles = {},
24 | hoverStyles = {},
25 | focusStyles = {},
26 | activeStyles = {},
27 | attrs = {},
28 | childrens = [],
29 | events = {},
30 | appendTo = document.body,
31 | afterChildrenAppend = null,
32 | afterCreated = null,
33 | visible = false
34 | } = {}) {
35 |
36 | super()
37 |
38 | this.id = id;
39 | this.tag = tag;
40 | this.styles = styles;
41 | this.hoverStyles = hoverStyles;
42 | this.focusStyles = focusStyles;
43 | this.activeStyles = activeStyles;
44 | this.attrs = attrs;
45 | this.childrens = childrens;
46 | this.events = events;
47 | this.appendTo = appendTo;
48 | this.afterChildrenAppend = afterChildrenAppend;
49 | this.afterCreated = afterCreated;
50 | this.visible = visible
51 |
52 | this.createElement();
53 | }
54 |
55 | createElement() {
56 | if (!this.tag) {
57 | throw new Error('Tag is required to create an element.');
58 | }
59 |
60 | this.el = document.createElement(this.tag);
61 | if (this.id) this.el.id = this.id;
62 | this.appendTo.appendChild(this.el);
63 |
64 | this.addStyles();
65 | this.addAttrs();
66 | this.addChildrens();
67 | this.addEvents();
68 | this.addStateStyles();
69 |
70 | if (!this.visible) {
71 | this.hide()
72 | }
73 |
74 | if (this.afterCreated) {
75 | this.afterCreated(this)
76 | }
77 | }
78 |
79 | removeElement() {
80 | if (!this.el) return;
81 | this.removeAttrs();
82 | this.removeEvents();
83 | this.removeStyles();
84 | this.removeChildrens();
85 | this.el.remove();
86 | this.el = null;
87 | }
88 |
89 | updateStyles(styles = {}) {
90 | this.styles = { ...this.styles, ...styles };
91 | this.addStyles();
92 | }
93 |
94 | addStyles() {
95 | if (!this.el) return;
96 | Object.entries(this.styles).forEach(([style, value]) => {
97 | if (style === 'display') {
98 | this.defaultDisplay = value;
99 | }
100 | this.el.style[style] = value;
101 | });
102 | }
103 |
104 | removeStyles() {
105 | if (!this.el) return;
106 | Object.keys(this.styles).forEach(style => {
107 | this.el.style[style] = '';
108 | });
109 | this.styles = {};
110 | }
111 |
112 | updateAttrs(attrs = {}) {
113 | this.attrs = { ...this.attrs, ...attrs };
114 | this.addAttrs();
115 | }
116 |
117 | addAttrs() {
118 | if (!this.el) return;
119 | Object.entries(this.attrs).forEach(([attr, value]) => {
120 | this.el.setAttribute(attr, value);
121 | });
122 | }
123 |
124 | removeAttrs() {
125 | if (!this.el) return;
126 | Object.keys(this.attrs).forEach(attr => {
127 | this.el.removeAttribute(attr);
128 | });
129 | this.attrs = {};
130 | }
131 |
132 | addChildrens() {
133 | if (!this.el) return;
134 | this.childrens.forEach(child => {
135 | if (typeof child === 'string') {
136 | this.el.insertAdjacentHTML('beforeend', child);
137 | } else if (child instanceof Node) {
138 | this.el.appendChild(child);
139 | } else if (child instanceof StyleInjector) {
140 | this.el.appendChild(child.el);
141 | } else if (child instanceof DynamicElement) {
142 | this.el.appendChild(child.el);
143 | }
144 | });
145 | if (this.afterChildrenAppend) {
146 | requestAnimationFrame(() => this.afterChildrenAppend(this.childrens, this));
147 | }
148 | }
149 |
150 | addChild(child) {
151 | if (!this.el) return;
152 | if (typeof child === 'string') {
153 | this.el.insertAdjacentHTML('beforeend', child);
154 | } else if (child instanceof Node) {
155 | this.el.appendChild(child);
156 | } else if (child instanceof DynamicElement) {
157 | this.el.appendChild(child.el);
158 | }
159 | if(!this.childrens.includes(child)) {
160 | this.childrens.push(child)
161 | }
162 | if (this.afterChildrenAppend) {
163 | this.afterChildrenAppend(this.childrens, this);
164 | }
165 | }
166 |
167 | removeChildrens() {
168 | if (!this.el) return;
169 | this.el.innerHTML = '';
170 | this.childrens = [];
171 | }
172 |
173 | addEvents() {
174 | if (!this.el) return;
175 | Object.entries(this.events).forEach(([event, handler]) => {
176 | this.el.addEventListener(event, (e) => { handler(e, this.parent) });
177 | });
178 | }
179 |
180 | removeEvents() {
181 | if (!this.el) return;
182 | Object.entries(this.events).forEach(([event, handler]) => {
183 | this.el.removeEventListener(event, handler);
184 | });
185 | this.events = {};
186 | }
187 |
188 | show() {
189 | if (!this.el) return;
190 | this.el.style.display = this.defaultDisplay;
191 | this.visible = true;
192 | }
193 |
194 | hide() {
195 | if (!this.el) return;
196 | this.el.style.display = 'none';
197 | this.visible = false;
198 | }
199 |
200 | addStateStyles() {
201 | if (!this.el) return;
202 |
203 | this.el.addEventListener('mouseenter', () => this.applyStyles(this.hoverStyles));
204 | this.el.addEventListener('mouseleave', () => this.applyStyles(this.styles));
205 | this.el.addEventListener('focus', () => this.applyStyles(this.focusStyles), true);
206 | this.el.addEventListener('blur', () => this.applyStyles(this.styles), true);
207 | this.el.addEventListener('mousedown', () => this.applyStyles(this.activeStyles));
208 | this.el.addEventListener('mouseup', () => this.applyStyles(this.styles));
209 | }
210 |
211 | applyStyles(stateStyles) {
212 | if (!this.visible) return
213 | Object.entries(stateStyles).forEach(([style, value]) => {
214 | this.el.style[style] = value;
215 | });
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/web/lib/nexus.mouse.js:
--------------------------------------------------------------------------------
1 | export class NexusMouseManager {
2 |
3 | hexColors = ["#1abc9c","#2ecc71","#3498db","#9b59b6","#f1c40f","#e67e22","#e74c3c","#ecf0f1"]
4 |
5 | createMouseSvg(color) {
6 | return ``
13 | }
14 |
15 | svgStringToImage(svgString) {
16 | const blob = new Blob([svgString], { type: 'image/svg+xml' });
17 | const url = URL.createObjectURL(blob);
18 | const image = new Image();
19 | image.src = url;
20 | return image;
21 | }
22 |
23 | hex2rgb(hex) {
24 | const r = parseInt(hex.slice(1, 3), 16);
25 | const g = parseInt(hex.slice(3, 5), 16);
26 | const b = parseInt(hex.slice(5, 7), 16);
27 | return { r, g, b };
28 | }
29 |
30 | getRandomHexColor() {
31 | return this.hexColors[Math.floor(Math.random() * this.hexColors.length)];
32 | }
33 |
34 | constructor(nexusSocket, app, fps) {
35 |
36 | this.color = this.getRandomHexColor()
37 |
38 | this.app = app
39 | this.fps = fps
40 |
41 | this.canSendMouse = false
42 |
43 | this.nexusSocket = nexusSocket
44 | this.nexusSocket.onConnected.push(this.handleConnect.bind(this))
45 | this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this))
46 |
47 | document.addEventListener("visibilitychange", this.handleVisiblityChange.bind(this))
48 | document.addEventListener('mousemove', this.handleSendMouseControl.bind(this));
49 | this.app.graph.list_of_graphcanvas[0].onDrawForeground = this.handlePointerDraw.bind(this)
50 |
51 | setInterval(this.handleSendMouse.bind(this), 1000 / this.fps)
52 |
53 | }
54 |
55 | throttle(func, limit) {
56 | let lastFunc;
57 | let lastRan;
58 | return function (...args) {
59 | const context = this;
60 | if (!lastRan) {
61 | func.apply(context, args);
62 | lastRan = Date.now();
63 | } else {
64 | clearTimeout(lastFunc);
65 | lastFunc = setTimeout(function () {
66 | if ((Date.now() - lastRan) >= limit) {
67 | func.apply(context, args);
68 | lastRan = Date.now();
69 | }
70 | }, limit - (Date.now() - lastRan));
71 | }
72 | };
73 | }
74 |
75 | handleCanvasDraw() {
76 | if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) {
77 | this.app.graph.list_of_graphcanvas[0].draw(true, true);
78 | }
79 | }
80 |
81 | handleConnect() {
82 | this.drawTimer = setInterval(this.handleCanvasDraw.bind(this), 1000 / this.fps)
83 | }
84 |
85 | handleMessageReceive(message) {
86 |
87 | let name = message.name;
88 | let from = message.from;
89 | let data = message.data
90 |
91 | if (name == "mouse") {
92 | let mouse = this.nexusSocket.userManager.get(from, "mouse");
93 | let pointer = this.nexusSocket.userManager.get(from, "pointer");
94 | if (!pointer) {
95 | this.nexusSocket.userManager.set(from, "pointer", this.svgStringToImage(this.createMouseSvg(data.mouse[3])));
96 | } else if (pointer && mouse && mouse[3] != data.mouse[3]) {
97 | this.nexusSocket.userManager.set(from, "pointer", this.svgStringToImage(this.createMouseSvg(data.mouse[3])));
98 | }
99 | this.nexusSocket.userManager.set(from, "mouse", data.mouse)
100 | } else if (name == "hide_mouse") {
101 | this.nexusSocket.userManager.set(from, "mouse", null)
102 | } else if (name== "mouse_view_toggle") {
103 | this.nexusSocket.userManager.set(from, "mouse_view", data.on)
104 | if(data.on == 0){
105 | this.nexusSocket.sendMessage("hide_mouse", { receiver: from }, false)
106 | }
107 | }
108 |
109 | }
110 |
111 | handleVisiblityChange() {
112 | if (document.visibilityState === "hidden") {
113 | clearInterval(this.drawTimer)
114 | this.nexusSocket.sendMessage("hide_mouse", {}, false)
115 | this.nexusSocket.sendMessage("afk", {}, false)
116 | } else {
117 | this.handleConnect()
118 | this.nexusSocket.sendMessage("online", {}, false)
119 | }
120 | }
121 |
122 | handleSendMouse = function () {
123 | if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) {
124 | this.mouse = this.app.graph.list_of_graphcanvas[0].canvas_mouse
125 | this.scale = this.app.graph.list_of_graphcanvas[0].ds.scale
126 | }
127 | this.mouse[3] = this.color
128 | this.mouse[4] = this.scale / 10
129 |
130 | if (this.canSendMouse) {
131 | let users = Object.keys(this.nexusSocket.userManager.users)
132 | for (let index = 0; index < users.length; index++) {
133 | let user_id = users[index]
134 | let view = this.nexusSocket.userManager.get(user_id, "mouse_view");
135 | if(view != null) {
136 | if(view == 1) {
137 | this.nexusSocket.sendMessage('mouse', { mouse: this.mouse, receiver: user_id }, false)
138 | }
139 | }
140 | else {
141 | this.nexusSocket.sendMessage('mouse', { mouse: this.mouse, receiver: user_id }, false)
142 | }
143 | }
144 | this.canSendMouse = false
145 | }
146 |
147 | }
148 |
149 | handleSendMouseControl() {
150 | this.canSendMouse = true
151 | }
152 |
153 | handlePointerDraw = function (ctx) {
154 | Object.keys(this.nexusSocket.userManager.users).forEach(id => {
155 | if (id != this.nexusSocket.uuid) {
156 |
157 | let mouse = this.nexusSocket.userManager.get(id, "mouse");
158 | let name = this.nexusSocket.userManager.get(id, "name") || "User";
159 | let pointerImage = this.nexusSocket.userManager.get(id, "pointer");
160 | let hidden = this.nexusSocket.userManager.get(id, "mouse_hidden");
161 |
162 | if(hidden == 0) {
163 | mouse = null
164 | }
165 |
166 | if (mouse && mouse.length > 0) {
167 |
168 | let mouseX = mouse[0] - 4;
169 | let mouseY = mouse[1] - 4;
170 | let scale = 1 - mouse[4];
171 |
172 | ctx.drawImage(pointerImage, mouseX, mouseY, 18, 18);
173 | ctx.font = '14px Arial';
174 | ctx.fillStyle = mouse[3];
175 | ctx.fillText(name, mouseX + 22, mouseY + 16);
176 |
177 | let color = this.hex2rgb(mouse[3])
178 |
179 | ctx.beginPath();
180 | ctx.arc(mouseX, mouseY, scale * 600, 0, 2 * Math.PI, false);
181 | ctx.strokeStyle = `rgba(${color.r},${color.g},${color.b},0.1)`;
182 | ctx.lineWidth = 2;
183 | ctx.stroke();
184 |
185 | }
186 | }
187 |
188 | });
189 | }
190 | }
--------------------------------------------------------------------------------
/web/lib/nexus.workflow.panel.js:
--------------------------------------------------------------------------------
1 | import { DynamicElement } from "./nexus.elements.js"
2 | import { StyleInjector } from "./nexus.styles.js";
3 |
4 | export class WorkflowPanel {
5 |
6 | constructor() {
7 |
8 | this.active = 0
9 |
10 | this.panel = new DynamicElement({
11 | id: 'nexus-workflow-panel',
12 | tag: 'div',
13 | visible: false,
14 | styles: {
15 | "width": "100%",
16 | "height": "100vh",
17 | "display": "flex",
18 | "align-items": "center",
19 | "justify-content": "center",
20 | "box-sizing": "border-box",
21 | "position": "absolute",
22 | "left": "0",
23 | "top": "0",
24 | "z-index": "10000"
25 | },
26 | childrens: [
27 | new DynamicElement({
28 | id: 'nexus-workflow-panel-table',
29 | tag: 'div',
30 | visible: true,
31 | styles: {
32 | "width": "512px",
33 | "display": "flex",
34 | "flex-direction": "column",
35 | "border-radius": "6px",
36 | "box-sizing": "border-box"
37 | },
38 | childrens: [
39 | new DynamicElement({
40 | id: 'nexus-workflow-panel-table-body',
41 | tag: 'div',
42 | visible: true,
43 | styles: {
44 | "width": "100%",
45 | "display": "flex",
46 | "flex-direction": "column",
47 | "align-items": "center",
48 | "justify-content": "flex-start",
49 | "color": "#a3a3a3",
50 | "font-family": "Calibri",
51 | "font-weight": "600",
52 | "gap": "12px",
53 | "padding-bottom": "12px",
54 | "box-sizing": "border-box",
55 | "background": "rgba(54,54,54,0.8)",
56 | "border-radius": "6px",
57 | "box-shadow": "4px 8px 8px rgba(0,0,0,0.5)",
58 | "max-height": "540px",
59 | "overflowY": "auto"
60 | }
61 | }),
62 | new StyleInjector({
63 | styles: {
64 | "#nexus-workflow-panel .table-row": {
65 | "width": "100%",
66 | "display": "flex",
67 | "align-items": "center",
68 | "justify-content": "center",
69 | },
70 | "#nexus-workflow-panel .table-row:first-child": {
71 | "width": "100%",
72 | "color": "#a3a3a3",
73 | "padding": "16px 0",
74 | "background": "rgb(0,0,0,0.6)",
75 | "border-top-left-radius": "6px",
76 | "border-top-right-radius": "6px",
77 | "position": "sticky",
78 | "top": "0",
79 | },
80 | "#nexus-workflow-panel .table-row .table-row-child": {
81 | "display": "flex",
82 | "justify-content": "flex-start",
83 | "gap": "12px",
84 | "padding-left": "12px",
85 | "width": "100%"
86 | },
87 | "#nexus-workflow-panel .table-row-child button": {
88 | "border": "none",
89 | "padding": "6px 12px",
90 | "border-radius": "3px",
91 | "cursor": "pointer",
92 | "background": "rgba(94,94,94)",
93 | "color": "#a1a1a1",
94 | "width": "100%"
95 | },
96 | "#nexus-workflow-panel .table-row-child:first-child": {
97 | "width":"50%"
98 | },
99 | "#nexus-workflow-panel .table-row-child:last-child": {
100 | "padding-right":"12px"
101 | },
102 | "#nexus-workflow-panel .table-row-child button:active": {
103 | "opacity": "0.8"
104 | },
105 | "#nexus-workflow-panel .table-row-child button[label='load']:active": {
106 | "background": "#218c74",
107 | "color": "#fafafa"
108 | },
109 | "#nexus-workflow-panel .table-row-child button[label='loadforall']:active": {
110 | "background": "#474787",
111 | "color": "#fafafa"
112 | }
113 | }
114 | })
115 | ]
116 | })
117 | ],
118 | })
119 | this.onCommandReceived = null
120 | this.buttons = []
121 | this.columns = []
122 | }
123 |
124 | addColumnsToPanel(columns) {
125 |
126 | this.columns = columns
127 |
128 | }
129 |
130 | addRowsToPanel(rows, callbacks, values, data) {
131 |
132 | let tbody = this.panel.childrens[0].childrens[0];
133 | tbody.removeChildrens();
134 | this.buttons = []
135 |
136 | const tr = document.createElement('div');
137 | tr.className = "table-row";
138 |
139 | this.columns.forEach(heading => {
140 | const th = document.createElement('span');
141 | th.className = "table-row-child"
142 | th.textContent = heading;
143 | tr.appendChild(th)
144 | });
145 |
146 | tbody.addChild(tr)
147 |
148 | rows.forEach((row, i) => {
149 | const tr = document.createElement('div');
150 | tr.className = "table-row";
151 |
152 | row.forEach((cell, j) => {
153 |
154 | let td = document.createElement('span');
155 | td.className = "table-row-child"
156 |
157 | let hascomma = cell.indexOf(",") > -1
158 | let hascolon = cell.indexOf(":") > -1
159 |
160 | let content = []
161 |
162 | if (hascomma) {
163 | content = cell.split(',')
164 | content.forEach((elm, k) => {
165 | let elmcontent = elm.split(':')
166 | this.addChildToParent(elmcontent, td, callbacks, values, data, i, j, k)
167 | })
168 | }
169 | else if (hascolon) {
170 | content = cell.split(':')
171 | this.addChildToParent(content, td, callbacks, values, data, i, j, -1)
172 | } else {
173 | cell = cell.split("|")
174 | cell = cell.join(":")
175 | td.textContent = cell
176 | }
177 | tr.appendChild(td);
178 |
179 | });
180 | tbody.addChild(tr)
181 | });
182 | }
183 |
184 | addChildToParent(content, td, callbacks, values, data, i, j, k) {
185 | switch (content[0]) {
186 | case 'button':
187 | let btn = document.createElement('button');
188 | if (content[1] == 'none') {
189 | btn.className = "none"
190 | }
191 | if (!this.buttons.includes(btn)) {
192 | this.buttons.push(btn)
193 | }
194 | btn.textContent = content[1]
195 | if (callbacks[i] && callbacks[i][j] && values[i][j] && data[i][j]) {
196 | let inner_values = values[i][j].split(',')
197 | let inner_data = data[i][j].split(',')
198 | if (k != -1) {
199 | btn.setAttribute('data', inner_data[k]);
200 | btn.setAttribute('active', inner_values[k]);
201 | } else {
202 | btn.setAttribute('data', data[i][j]);
203 | btn.setAttribute('active', values[i][j]);
204 | }
205 | btn.setAttribute('kind', content[0]);
206 | btn.setAttribute('label', content[1]);
207 | if (content[1] != 'none') {
208 | btn.onclick = callbacks[i][j].bind(this);
209 | }
210 | }
211 | td.appendChild(btn)
212 | break;
213 | }
214 |
215 | }
216 |
217 | onclick(e) {
218 |
219 | let kind = e.target.getAttribute('kind')
220 | let data = e.target.getAttribute('data')
221 | let label = e.target.getAttribute('label')
222 | let active = parseInt(e.target.getAttribute('active'))
223 | if (kind == "button") {
224 | if (active == 0) {
225 | e.target.setAttribute('active', 1)
226 | } else if (active == 1) {
227 | e.target.setAttribute('active', 0)
228 | }
229 | }
230 | active = parseInt(e.target.getAttribute('active'))
231 | //console.log(kind, label, data, active)
232 | if (this.onCommandReceived) {
233 | this.onCommandReceived(kind, label, data, active)
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/web/lib/nexus.comfy.js:
--------------------------------------------------------------------------------
1 | export class NexusPromptControl extends EventTarget {
2 |
3 | constructor(api, app, nexusSocket) {
4 |
5 | super()
6 |
7 | this.app = app
8 | this.api = api
9 | this.runningNodeId = null
10 | this.nexusSocket = nexusSocket
11 | this.comfy_events = ['b_preview', 'progress', 'executing', 'executed', 'execution_start', 'execution_error', 'execution_cached'];
12 |
13 | let oqp = api.queuePrompt;
14 | this.apiWrapper = {
15 | oqp: oqp,
16 | enableApi: false,
17 | api: api,
18 | qp: async function (number, data) {
19 | if (!this.enableApi) {
20 | return {
21 | "node_errors": {}
22 | };
23 | }
24 | return this.oqp.call(this.api, number, { output: data.output, workflow: data.workflow });
25 | }
26 | };
27 |
28 | this.api.queuePrompt = this.apiWrapper.qp.bind(this.apiWrapper);
29 | this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this))
30 |
31 | this.addEventListeners();
32 | this.comfy_events.forEach((event) => {
33 | this.api.addEventListener(event, this.handleComfyEvents.bind(this));
34 | });
35 |
36 | this.handleProgressDraw()
37 | this.handleControl(false)
38 | this.handleEditor(false)
39 |
40 | }
41 |
42 | handleProgressDraw() {
43 |
44 | const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
45 | const self = this.app;
46 | const self2 = this;
47 |
48 | LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
49 |
50 | const res = origDrawNodeShape.apply(this, arguments);
51 | const nodeErrors = self.lastNodeErrors?.[node.id];
52 |
53 | let color = null;
54 | let lineWidth = 1;
55 |
56 | if (node.id === +self2.runningNodeId) {
57 | color = "#0f0";
58 | } else if (self.dragOverNode && node.id === self.dragOverNode.id) {
59 | color = "dodgerblue";
60 | }
61 | else if (nodeErrors?.errors) {
62 | color = "red";
63 | lineWidth = 2;
64 | }
65 | else if (self.lastExecutionError && self.lastExecutionError.node_id === node.id) {
66 | color = "#f0f";
67 | lineWidth = 2;
68 | }
69 |
70 | if (color) {
71 | const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
72 | ctx.lineWidth = lineWidth;
73 | ctx.globalAlpha = 0.8;
74 | ctx.beginPath();
75 | if (shape == LiteGraph.BOX_SHAPE)
76 | ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
77 | else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed))
78 | ctx.roundRect(
79 | -6,
80 | -6 - LiteGraph.NODE_TITLE_HEIGHT,
81 | 12 + size[0] + 1,
82 | 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
83 | this.round_radius * 2
84 | );
85 | else if (shape == LiteGraph.CARD_SHAPE)
86 | ctx.roundRect(
87 | -6,
88 | -6 - LiteGraph.NODE_TITLE_HEIGHT,
89 | 12 + size[0] + 1,
90 | 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
91 | [this.round_radius * 2, this.round_radius * 2, 2, 2]
92 | );
93 | else if (shape == LiteGraph.CIRCLE_SHAPE)
94 | ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
95 | ctx.strokeStyle = color;
96 | ctx.stroke();
97 | ctx.strokeStyle = fgcolor;
98 | ctx.globalAlpha = 1;
99 | }
100 |
101 | if (self.progress && node.id === +self2.runningNodeId) {
102 | ctx.fillStyle = "green";
103 | ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6);
104 | ctx.fillStyle = bgcolor;
105 | }
106 |
107 | // Highlight inputs that failed validation
108 | if (nodeErrors) {
109 | ctx.lineWidth = 2;
110 | ctx.strokeStyle = "red";
111 | for (const error of nodeErrors.errors) {
112 | if (error.extra_info && error.extra_info.input_name) {
113 | const inputIndex = node.findInputSlot(error.extra_info.input_name)
114 | if (inputIndex !== -1) {
115 | let pos = node.getConnectionPos(true, inputIndex);
116 | ctx.beginPath();
117 | ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
118 | ctx.stroke();
119 | }
120 | }
121 | }
122 | }
123 |
124 | return res;
125 | }
126 |
127 | }
128 |
129 | handleMessageReceive(message) {
130 | try {
131 | this.dispatchEvent(new CustomEvent(message.name, { detail: message }));
132 | } catch (e) {
133 | console.error('Error handling message:', e);
134 | }
135 | }
136 |
137 | handleComfyEvents(evt) {
138 | if (evt.type === "b_preview") {
139 | this.blobToBase64(evt.detail).then((result) => {
140 | this.nexusSocket.sendMessage(evt.type, { detail: result }, false)
141 | });
142 | } else {
143 | this.nexusSocket.sendMessage(evt.type, { detail: evt.detail }, false)
144 | }
145 | }
146 |
147 | addEventListeners() {
148 |
149 | this.addEventListener("progress", ({ detail }) => {
150 | detail = detail.data.detail
151 | this.app.progress = detail;
152 | this.app.graph.setDirtyCanvas(true, false);
153 | });
154 |
155 | this.addEventListener("executing", ({ detail }) => {
156 | detail = detail.data.detail
157 | this.app.progress = null;
158 | this.runningNodeId = detail;
159 | this.app.graph.setDirtyCanvas(true, false);
160 | delete this.app.nodePreviewImages[this.runningNodeId];
161 | });
162 |
163 | this.addEventListener("executed", ({ detail }) => {
164 | detail = detail.data.detail
165 | const output = this.app.nodeOutputs[detail.node];
166 | if (detail.merge && output) {
167 | for (const k in detail.output ?? {}) {
168 | const v = output[k];
169 | if (v instanceof Array) {
170 | output[k] = v.concat(detail.output[k]);
171 | } else {
172 | output[k] = detail.output[k];
173 | }
174 | }
175 | } else {
176 | this.app.nodeOutputs[detail.node] = detail.output;
177 | }
178 | const node = this.app.graph.getNodeById(detail.node);
179 | if (node && node.onExecuted) {
180 | node.onExecuted(detail.output);
181 | }
182 | });
183 |
184 | this.addEventListener("execution_start", ({ detail }) => {
185 | detail = detail.data.detail
186 | this.runningNodeId = null;
187 | this.app.lastExecutionError = null;
188 | this.app.graph._nodes.forEach((node) => {
189 | if (node.onExecutionStart) {
190 | node.onExecutionStart();
191 | }
192 | });
193 | });
194 |
195 | this.addEventListener("execution_error", ({ detail }) => {
196 | detail = detail.data.detail
197 | this.app.lastExecutionError = detail;
198 | const formattedError = this.formatExecutionError(detail);
199 | this.app.ui.dialog.show(formattedError);
200 | this.app.canvas.draw(true, true);
201 | });
202 |
203 | this.addEventListener("b_preview", ({ detail }) => {
204 | detail = detail.data.detail
205 | const id = this.runningNodeId;
206 | if (id == null) return;
207 | const blob = this.base64ToBlob(detail);
208 | const blobUrl = URL.createObjectURL(blob);
209 | this.app.nodePreviewImages[id] = [blobUrl];
210 | });
211 | }
212 |
213 | blobToBase64(blob) {
214 | return new Promise((resolve, reject) => {
215 | const reader = new FileReader();
216 | reader.onloadend = () => {
217 | resolve(reader.result);
218 | };
219 | reader.onerror = reject;
220 | reader.readAsDataURL(blob);
221 | });
222 | }
223 |
224 | base64ToBlob(dataUrl) {
225 | const [header, base64] = dataUrl.split(',');
226 | const mime = header.match(/:(.*?);/)[1];
227 | const byteCharacters = atob(base64);
228 | const byteNumbers = new Array(byteCharacters.length);
229 | for (let i = 0; i < byteCharacters.length; i++) {
230 | byteNumbers[i] = byteCharacters.charCodeAt(i);
231 | }
232 | const byteArray = new Uint8Array(byteNumbers);
233 | return new Blob([byteArray], { type: mime });
234 | }
235 |
236 | formatExecutionError(error) {
237 | if (error == null) {
238 | return "(unknown error)";
239 | }
240 |
241 | const traceback = error.traceback.join("");
242 | const nodeId = error.node_id;
243 | const nodeType = error.node_type;
244 |
245 | return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`;
246 | }
247 |
248 | handleEditor(oN) {
249 |
250 | this.app.canvas.allow_dragnodes = oN;
251 | this.app.canvas.allow_interaction = oN;
252 | this.app.canvas.allow_reconnect_links = oN;
253 | this.app.canvas.allow_searchbox = oN;
254 |
255 | }
256 |
257 | handleControl(oN) {
258 | this.apiWrapper.enableApi = oN;
259 | }
260 |
261 | }
--------------------------------------------------------------------------------
/web/lib/nexus.panel.js:
--------------------------------------------------------------------------------
1 | import { DynamicElement } from "./nexus.elements.js"
2 | import { StyleInjector } from "./nexus.styles.js";
3 |
4 | export class UserPanel {
5 |
6 | constructor() {
7 |
8 | this.active = 0
9 |
10 | this.panel = new DynamicElement({
11 | id: 'nexus-player-panel',
12 | tag: 'div',
13 | visible: false,
14 | styles: {
15 | "width": "100%",
16 | "height": "100vh",
17 | "display": "flex",
18 | "align-items": "center",
19 | "justify-content": "center",
20 | "box-sizing": "border-box",
21 | "position": "absolute",
22 | "left": "0",
23 | "top": "0",
24 | "z-index": "10000"
25 | },
26 | childrens: [
27 | new DynamicElement({
28 | id: 'nexus-player-panel-table',
29 | tag: 'div',
30 | visible: true,
31 | styles: {
32 | "width": "832px",
33 | "display": "flex",
34 | "flex-direction": "column",
35 | "border-radius": "6px",
36 | "box-sizing": "border-box"
37 | },
38 | childrens: [
39 | new DynamicElement({
40 | id: 'nexus-player-panel-table-body',
41 | tag: 'div',
42 | visible: true,
43 | styles: {
44 | "width": "100%",
45 | "display": "flex",
46 | "flex-direction": "column",
47 | "align-items": "center",
48 | "justify-content": "flex-start",
49 | "color": "#a3a3a3",
50 | "font-family": "Calibri",
51 | "font-weight": "600",
52 | "gap": "12px",
53 | "padding-bottom": "12px",
54 | "box-sizing": "border-box",
55 | "background": "rgba(54,54,54,0.8)",
56 | "border-radius": "6px",
57 | "box-shadow": "4px 8px 8px rgba(0,0,0,0.5)",
58 | "max-height": "540px",
59 | "overflowY": "auto"
60 | }
61 | }),
62 | new StyleInjector({
63 | styles: {
64 | "#nexus-player-panel .table-row": {
65 | "width": "100%",
66 | "display": "grid",
67 | "align-items": "center",
68 | "grid-template-columns": "20% 60% 20%",
69 | "justify-content": "space-between",
70 | },
71 | "#nexus-player-panel .table-row:first-child": {
72 | "width": "100%",
73 | "color": "#a3a3a3",
74 | "padding": "16px 0",
75 | "background": "rgb(0,0,0,0.6)",
76 | "border-top-left-radius": "6px",
77 | "border-top-right-radius": "6px",
78 | "position": "sticky",
79 | "top": "0",
80 | },
81 | "#nexus-player-panel .table-row .table-row-child": {
82 | "display": "flex",
83 | "justify-content": "flex-start",
84 | "gap": "12px",
85 | "padding-left": "12px",
86 | "width": "100%"
87 | },
88 | "#nexus-player-panel .table-row-child button": {
89 | "border": "none",
90 | "padding": "6px 12px",
91 | "border-radius": "3px",
92 | "cursor": "pointer",
93 | "background": "rgba(94,94,94)",
94 | "color": "#a1a1a1",
95 | },
96 | "#nexus-player-panel .table-row-child button:active": {
97 | "opacity": "0.8"
98 | },
99 | "#nexus-player-panel .table-row-child button[active='1'][label='spectate']": {
100 | "background": "#218c74",
101 | "color": "#fafafa"
102 | },
103 | "#nexus-player-panel .table-row-child button[active='1'][label='editor']": {
104 | "background": "#474787",
105 | "color": "#fafafa"
106 | },
107 | "#nexus-player-panel .table-row-child button[active='1'][label='mouse']": {
108 | "background": "#cd6133",
109 | "color": "#fafafa"
110 | },
111 | "#nexus-player-panel .table-row-child button[active='1'][label='queue']": {
112 | "background": "#6F1E51",
113 | "color": "#fafafa"
114 | }
115 | }
116 | })
117 | ]
118 | })
119 | ],
120 | })
121 | this.onCommandReceived = null
122 | this.buttons = []
123 | this.columns = []
124 | }
125 |
126 | addColumnsToPanel(columns) {
127 |
128 | this.columns = columns
129 |
130 | }
131 |
132 | addRowsToPanel(rows, callbacks, values, data) {
133 |
134 | let tbody = this.panel.childrens[0].childrens[0];
135 | tbody.removeChildrens();
136 | this.buttons = []
137 |
138 | const tr = document.createElement('div');
139 | tr.className = "table-row";
140 |
141 | this.columns.forEach(heading => {
142 | const th = document.createElement('span');
143 | th.className = "table-row-child"
144 | th.textContent = heading;
145 | tr.appendChild(th)
146 | });
147 |
148 | tbody.addChild(tr)
149 |
150 | rows.forEach((row, i) => {
151 | const tr = document.createElement('div');
152 | tr.className = "table-row";
153 |
154 | row.forEach((cell, j) => {
155 |
156 | let td = document.createElement('span');
157 | td.className = "table-row-child"
158 |
159 | let hascomma = cell.indexOf(",") > -1
160 | let hascolon = cell.indexOf(":") > -1
161 |
162 | let content = []
163 |
164 | if (hascomma) {
165 | content = cell.split(',')
166 | content.forEach((elm, k) => {
167 | let elmcontent = elm.split(':')
168 | this.addChildToParent(elmcontent, td, callbacks, values, data, i, j, k)
169 | })
170 | }
171 | else if (hascolon) {
172 | content = cell.split(':')
173 | this.addChildToParent(content, td, callbacks, values, data, i, j, -1)
174 | } else {
175 | td.textContent = cell
176 | }
177 | tr.appendChild(td);
178 |
179 | });
180 | tbody.addChild(tr)
181 | });
182 | }
183 |
184 | addChildToParent(content, td, callbacks, values, data, i, j, k) {
185 | //console.log(content, td, callbacks, values, data, i, j, k)
186 | switch (content[0]) {
187 | case 'button':
188 | let btn = document.createElement('button');
189 | if (content[1] == 'none') {
190 | btn.className = "none"
191 | }
192 | if (!this.buttons.includes(btn)) {
193 | this.buttons.push(btn)
194 | }
195 | btn.textContent = content[1]
196 | if (callbacks[i] && callbacks[i][j] && values[i][j] && data[i][j]) {
197 | let inner_values = values[i][j].split(',')
198 | let inner_data = data[i][j].split(',')
199 | if (k != -1) {
200 | btn.setAttribute('data', inner_data[k]);
201 | btn.setAttribute('active', inner_values[k]);
202 | } else {
203 | btn.setAttribute('data', data[i][j]);
204 | btn.setAttribute('active', values[i][j]);
205 | }
206 | btn.setAttribute('kind', content[0]);
207 | btn.setAttribute('label', content[1]);
208 | if (content[1] != 'none') {
209 | btn.onclick = callbacks[i][j].bind(this);
210 | }
211 | }
212 | td.appendChild(btn)
213 | break;
214 | }
215 |
216 | }
217 |
218 | disableButtons(label) {
219 | this.buttons.forEach(btn => {
220 | let btn_kind = btn.getAttribute('kind')
221 | let btn_data = btn.getAttribute('data')
222 | let btn_active = parseInt(btn.getAttribute('active'))
223 | let btn_label = btn.getAttribute('label')
224 | if (btn_label == label) {
225 | if (btn_active == 1) {
226 | btn.setAttribute('active', 0)
227 | //console.log(btn_kind, btn_label, btn_data, 0)
228 | if (this.onCommandReceived) {
229 | this.onCommandReceived(btn_kind, btn_label, btn_data, 0)
230 | }
231 | }
232 | }
233 | })
234 | }
235 |
236 | onclick(e) {
237 |
238 | let kind = e.target.getAttribute('kind')
239 | let data = e.target.getAttribute('data')
240 | let label = e.target.getAttribute('label')
241 | let active = parseInt(e.target.getAttribute('active'))
242 | if (kind == "button") {
243 | if (label == "spectate") {
244 | this.disableButtons(label)
245 | }
246 | if (active == 0) {
247 | e.target.setAttribute('active', 1)
248 | } else if (active == 1) {
249 | e.target.setAttribute('active', 0)
250 | }
251 | }
252 | active = parseInt(e.target.getAttribute('active'))
253 | //console.log(kind, label, data, active)
254 | if (this.onCommandReceived) {
255 | this.onCommandReceived(kind, label, data, active)
256 | }
257 | }
258 | }
--------------------------------------------------------------------------------
/web/lib/nexus.workflow.js:
--------------------------------------------------------------------------------
1 | import "./nexus.litegraph.js"
2 | import { postWorkflow, postBackupWorkflow, getLatestWorkflow } from "./nexus.api.js";
3 |
4 | export class NexusWorkflowManager {
5 |
6 | constructor(app, api, nexusSocket) {
7 |
8 | this.app = app;
9 | this.api = api;
10 |
11 | this.nexusSocket = nexusSocket;
12 | this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this))
13 |
14 | this.init()
15 |
16 | this.spectators = []
17 | this.saveTime = 0
18 | setInterval(this.handleSpectators.bind(this), 1000 / 24) // 24 fps
19 |
20 | }
21 |
22 | async init() {
23 | let workflow = (await this.app.graphToPrompt()).workflow;
24 | await postBackupWorkflow(this.nexusSocket.uuid, workflow)
25 | this.app.graph.clear()
26 | }
27 |
28 | removeSaveManager() {
29 | if (this.saveTimer) {
30 | clearInterval(this.saveTimer)
31 | }
32 | // document.removeEventListener("visibilitychange", this.handleSaveWorkflowManager.bind(this))
33 | }
34 |
35 | async saveManager(time) {
36 | if (this.nexusSocket.admin) {
37 | this.nexusSocket.sendMessage('workflow:timer', { time: time - 2 }, false)
38 | }
39 | this.removeSaveManager()
40 | this.saveTime = time
41 | this.saveTimer = setInterval(this.sendWorkFlow.bind(this), time * 1000)
42 | console.log("Auto Backup Timer Setup:",time)
43 | // document.addEventListener("visibilitychange", this.handleSaveWorkflowManager.bind(this))
44 | }
45 |
46 | async sendWorkFlow() {
47 | let workflow = (await this.app.graphToPrompt()).workflow;
48 | await postWorkflow(this.nexusSocket.uuid, workflow)
49 | }
50 |
51 | async loadLatestWorkflow() {
52 | let workflow = await getLatestWorkflow()
53 | let links = workflow.links || [];
54 | let nodes = workflow.nodes || [];
55 | let groups = workflow.groups || [];
56 | this.app.graph.updateNodes(nodes);
57 | this.app.graph.updateLinks(links);
58 | this.app.graph.updateGroups(groups);
59 | this.app.graph.updateExecutionOrder();
60 | this.app.graph.setDirtyCanvas(true, true);
61 | }
62 |
63 | async handleSaveWorkflowManager() {
64 | this.sendWorkFlow()
65 | // if (document.visibilityState === "hidden") {
66 | // if (this.saveTimer) {
67 | // clearInterval(this.saveTimer)
68 | // }
69 | // }
70 | // else {
71 | // this.saveTimer = setInterval(this.sendWorkFlow.bind(this), this.saveTime * 1000)
72 | // }
73 | }
74 |
75 | async handleSpectators() {
76 | if (this.spectators.length > 0) {
77 | let workflow = (await this.app.graphToPrompt()).workflow;
78 | let wfe = workflow.extra
79 | for (let index = 0; index < this.spectators.length; index++) {
80 | const id = this.spectators[index];
81 | this.nexusSocket.sendMessage('workflow:spectate', { workflow: wfe, receiver: id }, false)
82 | }
83 | }
84 | }
85 |
86 | async handleMessageReceive(message) {
87 |
88 | let from = message.from;
89 | let data = message.data
90 | let name = message.name;
91 |
92 | switch (name) {
93 | case "spectate":
94 | if (data.on == 0) {
95 | if (this.spectators.includes(from)) {
96 | this.spectators.splice(this.spectators.indexOf(from), 1)
97 | }
98 | } else if (data.on == 1) {
99 | if (!this.spectators.includes(from)) {
100 | this.spectators.push(from)
101 | }
102 | }
103 | case "join":
104 | // this.sendWorkFlow()
105 | this.saveManager(this.saveTime || 60)
106 | break;
107 | case "workflow:spectate":
108 | let wfe = data.workflow
109 | if (wfe?.ds) {
110 | this.app.canvas.ds.offset = wfe.ds.offset;
111 | this.app.canvas.ds.scale = wfe.ds.scale;
112 | }
113 | break;
114 | case "workflow:timer":
115 | let time = data.time
116 | this.saveManager(time)
117 | break;
118 | case "workflow:update":
119 | this.handleWorkflowUpdate(from, data)
120 | break;
121 | case "workflow":
122 | let workflow = data.workflow
123 | if (workflow.clear) {
124 | await this.app.graph.clear()
125 | }
126 | let links = workflow.links || [];
127 | let nodes = workflow.nodes || [];
128 | let groups = workflow.groups || [];
129 | this.app.graph.updateNodes(nodes);
130 | this.app.graph.updateLinks(links);
131 | this.app.graph.updateGroups(groups);
132 | this.app.graph.updateExecutionOrder();
133 | this.app.graph.setDirtyCanvas(true, true);
134 | }
135 |
136 | }
137 |
138 | handleWorkflowEvents(oN) {
139 | if (oN) {
140 | this.app.graph.onNodeAdded = this.onNodeAdded.bind(this);
141 | this.app.graph.onNodeAddedNexus = this.onNodeAddedNexus.bind(this);
142 | this.app.graph.onNodeRemoved = this.onNodeRemoved.bind(this);
143 | this.app.canvas.onSelectionChange = this.onSelectionChange.bind(this);
144 | this.app.canvas.onNodeDeselected = this.onNodeDeselected.bind(this);
145 | this.app.canvas.onNodeMoved = this.onNodeMoved.bind(this);
146 | this.app.graph.onGroupAdded = this.onGroupAdded.bind(this);
147 | this.app.graph.onGroupRemoved = this.onGroupRemoved.bind(this);
148 | this.app.graph._groups.forEach(group => {
149 | group.onGroupMoved = this.onGroupMoved.bind(this, group);
150 | group.onGroupNodeMoved = this.onNodeMoved.bind(this);
151 | });
152 | this.app.graph._nodes.forEach(node => {
153 | node.onWidgetChanged = this.onWidgetChanged.bind(this, node);
154 | node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node);
155 | });
156 | }
157 | else {
158 | this.app.graph.onNodeAddedNexus = null;
159 | this.app.graph.onNodeAdded = null;
160 | this.app.graph.onNodeRemoved = null;
161 | this.app.canvas.onSelectionChange = null;
162 | this.app.canvas.onNodeDeselected = null;
163 | this.app.canvas.onNodeMoved = null;
164 | this.app.graph.onGroupAdded = null;
165 | this.app.graph.onGroupRemoved = null;
166 | this.app.graph._groups.forEach(group => {
167 | group.onGroupMoved = null;
168 | group.onGroupNodeMoved = null;
169 | });
170 | this.app.graph._nodes.forEach(node => {
171 | node.onWidgetChanged = null;
172 | node.onConnectionsChange = null;
173 | });
174 | }
175 | }
176 |
177 | handleWorkflowUpdate(from, detail) {
178 |
179 | let graph = this.app.graph;
180 | let canvas = this.app.canvas;
181 | let update = detail.update;
182 | let data = detail.data;
183 |
184 | let existingNode;
185 |
186 | switch (update) {
187 | case "node-added":
188 | graph.updateNode(data);
189 | break;
190 | case "node-removed":
191 | graph.removeNode(data);
192 | break;
193 | case "node-moved":
194 | graph.updateNode(data);
195 | break;
196 | case "node-widget-changed":
197 | graph.updateNode(data);
198 | break;
199 | case "node-selection":
200 | let nodes = []
201 | data.forEach(node => {
202 | existingNode = graph.getNodeById(node.id);
203 | if (existingNode) {
204 | nodes.push(existingNode)
205 | }
206 | })
207 | canvas.selectNodesNexus(nodes, true)
208 | break;
209 | case "node-deselection":
210 | existingNode = graph.getNodeById(data.id);
211 | if (existingNode)
212 | canvas.deselectNodeNexus(existingNode)
213 | break;
214 | case "group-added":
215 | graph.updateGroup(data);
216 | break;
217 | case "group-moved":
218 | graph.updateGroup(data);
219 | break;
220 | case "group-removed":
221 | graph.removeGroup(data);
222 | break;
223 | case "node-connection":
224 | graph.updateLinkManual(detail.change, detail.link, detail.node, graph);
225 | break;
226 | default:
227 | //console.log(update);
228 | break;
229 | }
230 |
231 | }
232 |
233 | onNodeAddedNexus(node) {
234 | node.onWidgetChanged = this.onWidgetChanged.bind(this, node);
235 | node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node);
236 | }
237 |
238 | onNodeAdded(node) {
239 | let data = node.serialize();
240 | node.onWidgetChanged = this.onWidgetChanged.bind(this, node);
241 | node.onConnectionsChange = this.onNodeConnectionChange.bind(this, node);
242 | this.nexusSocket.sendMessage('workflow:update', { update: "node-added", data }, false);
243 | }
244 |
245 | onWidgetChanged(node) {
246 | let data = node.serialize();
247 | this.nexusSocket.sendMessage('workflow:update', { update: "node-widget-changed", data }, false);
248 | }
249 |
250 | onNodeRemoved(node) {
251 | let data = node.serialize();
252 | this.nexusSocket.sendMessage('workflow:update', { update: "node-removed", data }, false);
253 | }
254 |
255 | async onNodeConnectionChange(node, a, b, c, d, e) {
256 | let change = c ? "add" : "remove";
257 | let link = d;
258 | this.graphData = (await this.app.graphToPrompt()).workflow;
259 | let data = this.graphData.links;
260 | //console.log(data)
261 | node = this.app.graph.getNodeById(node.id);
262 | node = node ? node.serialize() : null;
263 | this.nexusSocket.sendMessage('workflow:update', { node, update: "node-connection", link, change, data }, false);
264 | }
265 |
266 | onSelectionChange(nodes) {
267 | let data = []
268 | Object.keys(nodes).forEach(node_id => {
269 | data.push(nodes[node_id].serialize())
270 | })
271 | this.nexusSocket.sendMessage('workflow:update', { update: "node-selection", data }, false);
272 | }
273 |
274 | onNodeDeselected(node) {
275 | let data = node.serialize()
276 | this.nexusSocket.sendMessage('workflow:update', { update: "node-deselection", data }, false);
277 | }
278 |
279 | onNodeMoved(node) {
280 | let data = node.serialize();
281 | this.nexusSocket.sendMessage('workflow:update', { update: "node-moved", data }, false);
282 | }
283 |
284 | onGroupAdded(group) {
285 | let data = group.serialize();
286 | let uuid = localStorage.getItem('nexus-socket-uuid');
287 | if (uuid) {
288 | data.owner = uuid
289 | group.configure(data);
290 | }
291 | group.onGroupMoved = this.onGroupMoved.bind(this, group);
292 | group.onGroupNodeMoved = this.onNodeMoved.bind(this);
293 | this.nexusSocket.sendMessage('workflow:update', { update: "group-added", data }, false);
294 | }
295 |
296 | onGroupRemoved(group) {
297 | let data = group.serialize();
298 | this.nexusSocket.sendMessage('workflow:update', { update: "group-removed", data }, false);
299 | }
300 |
301 | onGroupMoved(group) {
302 | let data = group.serialize();
303 | this.nexusSocket.sendMessage('workflow:update', { update: "group-moved", data }, false);
304 | }
305 |
306 | }
307 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | # ComfyUI-Nexus
5 |
6 |  
7 |
8 | A ComfyUI node designed to enable seamless multi-user workflow collaboration.
9 |
10 | 
11 |
12 | **Features Video**: https://www.youtube.com/watch?v=RnYIUG59oTM
13 |
14 |
15 |
16 | # Important Notes
17 |
18 |
19 |
20 | - **Install/Maintain on Server Only**: This node should only be installed on the server machine.
21 | - **No Installation Needed for Others**: Other users don’t need to install this node.
22 | - **URL for Connection**: Other users only need the URL to connect locally/remotely.
23 |
24 | - **Security**:
25 | - ComfyUI menu and features are for admins only.
26 | - ComfyUI shortcuts are for admins only.
27 | - Prompt Queue shortcut `CTRL+Enter` is for users with queue permission only.
28 |
29 | - **Editor Permissions**:
30 | - Editors can only edit the graph (create/update/delete/move).
31 | - If an editor has queue permission, they can queue prompts in the workflow.
32 |
33 | - **All Admin Server (not recommended)**:
34 | - One can create a server with all admins to resolve permission issues.
35 | - Refer to the `Admin Account Setup` section for more details.
36 |
37 |
38 |
39 | > [!WARNING]
40 | > When opening the ComfyUI workspace for the first time, it will be locked. Login as admin to enable editing.
41 |
42 | > [!WARNING]
43 | > Move or disable the ComfyUI-Nexus nodes from the custom nodes folder if you want to return to your normal ComfyUI setup.
44 |
45 |
46 |
47 | > [!CAUTION]
48 | > Enable the old `litegraph(legacy)` node search box. (New node search box is under development and has bugs)
49 |
50 | 
51 |
52 |
53 |
54 | ### Location of Nexus folder
55 |
56 | #### ComfyUI Folder
57 | - `Drive:/ComfyUI_windows_portable/nexus`
58 |
59 | #### Stable Matrix
60 | - **Full Version**: `Drive:/StabilityMatrix/Packages/ComfyUI/nexus`
61 | - **Portable Version**: `Drive:/StabilityMatrix/Data/Packages/ComfyUI/nexus`
62 |
63 |
64 |
65 | ## Disabling ComfyUI-Nexus
66 |
67 |
68 |
69 | - Stop ComfyUI and go to `ComfyUI\custom_nodes` folder
70 | - Rename `ComfyUI-Nexus` like this `ComfyUI-Nexus.disabled` to disable.
71 | - Restart ComfyUI again.
72 |
73 |
74 |
75 | ## Key Features
76 |
77 |
78 |
79 | - **Multiuser collaboration**: enable multiple users to work on the same workflow simultaneously.
80 | - **Local and Remote access**: use tools like ngrok or other tunneling software to facilitate remote collaboration. A local IP address on WiFi will also work 😎.
81 | - **Enhanced teamwork**: streamline your team's workflow management and collaboration process.
82 | - **Real-time chat**: communicate directly within the platform to ensure smooth and efficient collaboration.
83 | - **Spectate mode**: allow team members to observe the workflow in real-time without interfering—perfect for training or monitoring progress.
84 | - **Admin permissions**: admins can control who can edit the workflow and who can queue prompts, ensuring the right level of access for each team member.
85 | - **Workflow backup**: in case of any mishap, you can reload an old backup. The node saves 5 workflows, each 60 seconds apart.
86 |
87 |
88 |
89 | ## Key Binds
90 |
91 | - **Activate chat**: press **`t`**
92 | - **Show/hide users panel**: press **`LAlt + p`**
93 | - **Show/hide backups panel**: press **`LAlt + o`** (for user with editor permission only)
94 | - **Queue promt**: press **`CTRL+Enter`** (for user with queue permission only)
95 |
96 |
97 |
98 | ## Chat Commands
99 |
100 | - `/nick `: changes your nickname
101 | - `/login account password`: this command is used to become admin.
102 | - `/logout`: logout the admin.
103 |
104 |
105 |
106 | # Node Installation
107 |
108 | - ### Installing Using `comfy-cli`
109 | - `comfy node registry-install ComfyUI-Nexus`
110 | - https://registry.comfy.org/publishers/daxcay/nodes/comfyui-nexus
111 |
112 | - ### Manual Method
113 | - Go to your `ComfyUI\custom_nodes` and Run CMD.
114 | - Copy and paste this command: `git clone https://github.com/daxcay/ComfyUI-Nexus.git`
115 |
116 | - ### Automatic Method with [Comfy Manager](https://github.com/ltdrdata/ComfyUI-Manager)
117 | - Inside ComfyUI > Click the Manager Button on the side.
118 | - Click `Custom Nodes Manager` and search for `ComfyUI-Nexus`, then install this node.
119 |
120 |
121 |
122 | >[!IMPORTANT]
123 | > #### **Restart ComfyUI before proceeding to next step**
124 |
125 |
126 |
127 | # Server Setup
128 |
129 |
130 |
131 | ### Knowing ComfyUI Port
132 |
133 | - Open Comfyui in your browser:
134 |
135 | 
136 |
137 | - In your url tab, digits after colon (:) is your port.
138 |
139 | **Example:**
140 |
141 | 
142 |
143 | The port for the above URL will be **8188**
144 |
145 |
146 |
147 | ### Admin Account Setup
148 |
149 | - Open the file `ComfyUI\nexus\admins.json` in notepad.
150 |
151 | 
152 |
153 | - **"epic"** is the account name and **"comfynexus"** is password
154 | - Replace account and password with your own liking, but make sure not to use spaces.
155 |
156 | ### More than 1 Admin Account Setup
157 |
158 | - Open the file `ComfyUI\nexus\admins.json` in notepad. add another account(s) and password(s) like this.
159 |
160 | 
161 |
162 | - Make sure every password is different, and make sure not to use spaces.
163 |
164 |
165 |
166 | ### Setting run_nvidia_gpu.bat (Local setup if not using Tunneling Software)
167 |
168 | - Open *run_nvidia_gpu.bat* and write the following and save it:
169 |
170 | ```.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --disable-auto-launch --enable-cors-header "*"```
171 |
172 |
173 |
174 | - For a host machine in another IP Address write and save it:
175 |
176 | ```.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --disable-auto-launch --enable-cors-header "*" --listen 0.0.0.0```
177 |
178 |
179 |
180 | >[!IMPORTANT]
181 | >Don't leave the password as "comfynexus" as anyone can login.
182 |
183 | >[!NOTE]
184 | >**DO NOT SHARE ACCOUNT AND PASSWORD IN PUBLIC**
185 |
186 | >[!IMPORTANT]
187 | > #### Save file and **Restart ComfyUI before proceeding to next step**
188 |
189 |
190 |
191 | # Hosting Setup
192 |
193 | - One can use Ngrok or any other tunneling software supporting http/https to host a comfyui server remotely.
194 | - Also you can host locally over WiFi/Lan.
195 |
196 | ### Using Ngrok:
197 |
198 | - Go to this https://dashboard.ngrok.com/signup?ref=home-hero to sign up.
199 | - After signing up and logging in, go to this https://dashboard.ngrok.com/get-started/setup/windows to set up ngrok.
200 | - After installing and setting up ngrok,
201 | - Run CMD and enter this command: `ngrok http `
202 |
203 | 
204 | 
205 |
206 | - **Forwarding** is the Remote URL, Share this URL with your peers.
207 |
208 |
209 |
210 | ### Using Local IP
211 |
212 | - Open a cmd and write `ipconfig`.
213 |
214 | 
215 |
216 | - Now copy IPv4 address ad add comfy port to it. For example, if it's `http://192.168.1.45:`, the final URL will be: `http://192.168.1.45:5000`
217 | - Share this URL with your peers.
218 |
219 |
220 |
221 | >[!NOTE]
222 | > **Ngrok and WiFi address might change if you restart the machine. Follow above steps again to get the new URL.**
223 |
224 |
225 |
226 | ## Permissions in ComfyUI-Nexus
227 |
228 | - **viewer**: default permission given to a person joining the server.
229 | - **editor**: person having editor permission cad edit the workflow.
230 | - **queue prompt**: person having queue permission can queue the workflow.
231 |
232 |
233 | >[!NOTE]
234 | > Admin has all permissions by default.
235 |
236 |
237 |
238 | ## Real-Time Chat Window
239 |
240 | When you join ComfyUI for the first time, you will see this chat window in the top left corner:
241 |
242 | 
243 |
244 |
245 |
246 | To chat, press `t`, then write the message and press 'Enter'.
247 |
248 |
249 |
250 | ### Chat Commands
251 |
252 | - `/nick `: changes your nickname
253 | - `/login `: this command is used to become admin. **( account name and password saved in `admins.json` above )**
254 | - `/logout`: logout the admin.
255 |
256 |
257 |
258 | ## User Panel
259 |
260 | To show/hide the user panel, press `LShift+ LAlt + p`.
261 |
262 | **For users, the user panel will look like this:**
263 |
264 | 
265 |
266 | Users can perform the following actions on a joined user:
267 |
268 | 
269 |
270 | - **mouse**: show/hide the mouse of other players.
271 | - **spectate**: enable/disable spectate mode. Main use case: when you want to see or learn something from another user.
272 |
273 | **For admins, the user panel will look like this:**
274 |
275 | 
276 |
277 | Admins can perform the following actions on a joined user:
278 |
279 | 
280 |
281 | - **spectate**: enable/disable spectate mode. Main use case: when you want to see or learn something from another user.
282 | - **editor**: give/revoke editor permission to/from that user. Anyone with this permission can edit the workflow.
283 | - **queue**: give/revoke queue permission to/from that user. Anyone with this permission can queue the workflow.
284 | - **mouse**: show/hide the mouse of other players.
285 |
286 |
287 |
288 | ## Backup Panel (For Admins and Editors Only)
289 |
290 | To show/hide the backup panel, press `LShift + LAlt + o`.
291 |
292 | **The backup panel looks like this:**
293 |
294 | ## OLD
295 | 
296 |
297 | ## NEW
298 | 
299 |
300 | - **load**: load the backup on ComfyUI. If the admin presses it, it will load for all users.
301 |
302 |
303 |
304 | Backups are now divided into **'Short Term'** and **'Long Term'**
305 |
306 | **Short Term**: These are only 5 backups and are saved 60 seconds apart.
307 |
308 | **Long Term**: These backups are created every reload and they are never overwritten.
309 |
310 | > Backups are saved 60 seconds apart. To load a workflow dragged by an admin, the admin will have to wait 60 seconds to let the server make a backup, then load it for all users.
311 |
312 |
313 |
314 | ## Inspiration
315 |
316 | - Greatly Inspired by https://multitheftauto.com/ a GTA:SA multiplayer mod I spent years playing ❤️.
317 |
318 | ## Future Updates
319 |
320 | - Based on feedback, I will add/update features.
321 | - Multi-room collaboration.
322 | - Users can set their own color for names and mouse cursors.
323 |
324 |
325 |
326 | ### Daxton Caylor - ComfyUI Node Developer
327 |
328 | - ### Contact
329 | - **Email** - daxtoncaylor+Github@gmail.com
330 | - **Discord Server**: https://discord.gg/UyGkJycvyW
331 |
332 | - ### Support
333 | - **Patreon**: https://patreon.com/daxtoncaylor
334 | - **Buy me a coffee**: https://buymeacoffee.com/daxtoncaylor
335 |
336 |
337 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json
3 | import os
4 | import shutil
5 | import datetime
6 | import time
7 | import uuid
8 | from aiohttp import web
9 | from server import PromptServer
10 | import folder_paths
11 |
12 | from .classes.WebSocketManager import WebSocketManager
13 |
14 | DATA_FOLDER = f"{folder_paths.base_path}/nexus"
15 |
16 | PERMISSIONS_FILE = os.path.join(DATA_FOLDER, 'permissions.json')
17 | ADMINS_FILE = os.path.join(DATA_FOLDER, 'admins.json')
18 | TOKENS_FILE = os.path.join(DATA_FOLDER, 'tokens.json')
19 | NAMES_FILE = os.path.join(DATA_FOLDER, 'names.json')
20 |
21 | SERVER_FILE = os.path.join(DATA_FOLDER, 'server.json')
22 | WORKFLOWS_DIR = os.path.join(DATA_FOLDER, 'workflows')
23 |
24 | if not os.path.exists(DATA_FOLDER):
25 | os.makedirs(DATA_FOLDER)
26 |
27 | if not os.path.exists(ADMINS_FILE):
28 | with open(ADMINS_FILE, 'w') as f:
29 | admins_data = {'epic': 'comfynexus'}
30 | json.dump(admins_data, f, indent=4)
31 |
32 | for file_path in [PERMISSIONS_FILE, ADMINS_FILE, TOKENS_FILE, NAMES_FILE]:
33 | if not os.path.exists(file_path):
34 | with open(file_path, 'w') as f:
35 | json.dump({}, f)
36 |
37 | if not os.path.exists(WORKFLOWS_DIR):
38 | os.makedirs(WORKFLOWS_DIR)
39 |
40 | if not os.path.exists(SERVER_FILE):
41 | with open(SERVER_FILE, 'w') as f:
42 | default_config = {
43 | "name": "Default Nexus Server",
44 | "max_user": 15,
45 | "config": {
46 | "last_saved_user_id": "id"
47 | }
48 | }
49 | json.dump(default_config, f, indent=4)
50 |
51 |
52 | if hasattr(PromptServer, 'instance'):
53 | server = PromptServer.instance
54 | routes = web.RouteTableDef()
55 | socket_manager = WebSocketManager()
56 |
57 | async def broadcast_message(message, sender):
58 | for id, socket in socket_manager.sockets.items():
59 | if id != sender:
60 | try:
61 | await socket.send_str(json.dumps(message))
62 | except Exception as e:
63 | logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}")
64 |
65 | async def broadcast_message_except(message, excluder):
66 | for id, socket in socket_manager.sockets.items():
67 | if id != excluder:
68 | try:
69 | await socket.send_str(json.dumps(message))
70 | except Exception as e:
71 | logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}")
72 |
73 | async def broadcast_message_all(message):
74 | for id, socket in socket_manager.sockets.items():
75 | try:
76 | await socket.send_str(json.dumps(message))
77 | except Exception as e:
78 | logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}")
79 |
80 | async def send_message(message, sender, receiver):
81 | for id, socket in socket_manager.sockets.items():
82 | if id == receiver and sender != receiver:
83 | try:
84 | await socket.send_str(json.dumps(message))
85 | except Exception as e:
86 | logging.error(f"[ComfyUI-Nexus]: Error sending message to {id}: {e}")
87 |
88 | @routes.get("/nexus")
89 | async def websocket_handler(request):
90 | id = request.query.get("id")
91 | socket = web.WebSocketResponse()
92 | await socket.prepare(request)
93 |
94 | socket_manager.set(id, socket)
95 | logging.info(f"[ComfyUI-Nexus]: Socket {id} connected")
96 |
97 | try:
98 | async for msg in socket:
99 | if msg.type == web.WSMsgType.TEXT:
100 |
101 | data = json.loads(msg.data)
102 | event_name = data.get('name')
103 | event_data = data.get('data')
104 |
105 | sender = data.get("from")
106 | all = data.get('all')
107 | receiver = data.get('receiver')
108 | exclude = data.get('exclude')
109 |
110 | # logging.info(f"[ComfyUI-Nexus]: Received {sender} {all} {receiver} {exclude}")
111 |
112 | if event_name == "chat-join":
113 | user_id = data.get("from")
114 | user_name = event_data.get("name")
115 |
116 | with open(NAMES_FILE, 'r+') as f:
117 | names = json.load(f)
118 | names[user_id] = user_name
119 | f.seek(0)
120 | json.dump(names, f, indent=4)
121 | f.truncate()
122 |
123 | if event_name == "nick":
124 | user_id = data.get("from")
125 | user_name = event_data.get("new_name")
126 |
127 | with open(NAMES_FILE, 'r+') as f:
128 | names = json.load(f)
129 | names[user_id] = user_name
130 | f.seek(0)
131 | json.dump(names, f, indent=4)
132 | f.truncate()
133 |
134 | if all:
135 | await broadcast_message_all(data)
136 | elif receiver:
137 | await send_message(data, sender, receiver)
138 | elif exclude:
139 | await broadcast_message_except(data, exclude)
140 | else:
141 | await broadcast_message(data, sender)
142 |
143 | elif msg.type == web.WSMsgType.ERROR:
144 | logging.error(f"[ComfyUI-Nexus]: WebSocket error: {msg.exception()}")
145 | except Exception as e:
146 | logging.error(f"[ComfyUI-Nexus]: WebSocket error: {e}")
147 | finally:
148 | logging.info(f"[ComfyUI-Nexus]: Socket {id} disconnected")
149 | socket_manager.delete(id)
150 |
151 | return socket
152 |
153 | @routes.post("/nexus/workflows")
154 | async def get_user_specific_workflows(request):
155 |
156 | user_id = (await request.json()).get('user_id')
157 | if not user_id:
158 | return web.json_response({'error': 'User ID required'}, status=400)
159 |
160 | with open(PERMISSIONS_FILE, 'r') as f:
161 | permissions = json.load(f)
162 |
163 | user_permissions = permissions.get(user_id, {})
164 | if not user_permissions.get('editor', False):
165 | return web.json_response({'error': 'Forbidden'}, status=403)
166 |
167 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id)
168 | if not os.path.exists(user_workflow_dir):
169 | return web.json_response({'error': 'User not found'}, status=404)
170 |
171 | workflows = {}
172 | for file_name in sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int):
173 | file_path = os.path.join(user_workflow_dir, file_name)
174 | with open(file_path, 'r') as f:
175 | workflows[file_name] = json.load(f)
176 |
177 | for file_name in sorted([f for f in os.listdir(user_workflow_dir) if not f.isdigit()]):
178 | file_path = os.path.join(user_workflow_dir, file_name)
179 | with open(file_path, 'r') as f:
180 | workflows[file_name] = json.load(f)
181 |
182 | return web.json_response(workflows)
183 |
184 | @routes.post("/nexus/workflows/meta")
185 | async def get_user_specific_workflows_meta(request):
186 |
187 | user_id = (await request.json()).get('user_id')
188 | if not user_id:
189 | return web.json_response({'error': 'User ID required'}, status=400)
190 | with open(PERMISSIONS_FILE, 'r') as f:
191 | permissions = json.load(f)
192 |
193 | user_permissions = permissions.get(user_id, {})
194 | if not user_permissions.get('editor', False):
195 | return web.json_response({'error': 'Forbidden'}, status=403)
196 |
197 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id)
198 | if not os.path.exists(user_workflow_dir):
199 | return web.json_response({'error': 'User not found'}, status=404)
200 |
201 | meta_data = []
202 | for file_name in sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int):
203 | file_path = os.path.join(user_workflow_dir, file_name)
204 | last_modified_time = os.path.getmtime(file_path)
205 | last_modified_readable = datetime.datetime.fromtimestamp(last_modified_time).strftime('%Y-%m-%d %H:%M:%S')
206 |
207 | meta_data.append({
208 | 'file_name': file_name,
209 | 'last_saved': last_modified_readable
210 | })
211 |
212 | for file_name in sorted([f for f in os.listdir(user_workflow_dir) if not f.isdigit()]):
213 | file_path = os.path.join(user_workflow_dir, file_name)
214 | last_modified_time = os.path.getmtime(file_path)
215 | last_modified_readable = datetime.datetime.fromtimestamp(last_modified_time).strftime('%Y-%m-%d %H:%M:%S')
216 |
217 | meta_data.append({
218 | 'file_name': file_name,
219 | 'last_saved': last_modified_readable
220 | })
221 |
222 | return web.json_response(meta_data)
223 |
224 | @routes.post("/nexus/workflows/{id}")
225 | async def get_user_specific_workflow(request):
226 | user_id = (await request.json()).get('user_id')
227 | workflow_id = request.match_info.get('id')
228 |
229 | if not user_id:
230 | return web.json_response({'error': 'User ID required'}, status=400)
231 | if not workflow_id:
232 | return web.json_response({'error': 'Workflow ID required'}, status=400)
233 |
234 | with open(PERMISSIONS_FILE, 'r') as f:
235 | permissions = json.load(f)
236 |
237 | user_permissions = permissions.get(user_id, {})
238 | if not user_permissions.get('editor', False):
239 | return web.json_response({'error': 'Forbidden'}, status=403)
240 |
241 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id)
242 | if not os.path.exists(user_workflow_dir):
243 | return web.json_response({'error': 'User not found'}, status=404)
244 |
245 | workflow_file_path = os.path.join(user_workflow_dir, workflow_id)
246 | if not os.path.exists(workflow_file_path):
247 | return web.json_response({'error': 'Workflow not found'}, status=404)
248 |
249 | with open(workflow_file_path, 'r') as f:
250 | workflow = json.load(f)
251 |
252 | return web.json_response(workflow)
253 |
254 | @routes.post("/nexus/workflow")
255 | async def post_workflow(request):
256 | user_id = (await request.json()).get('user_id')
257 | if not user_id:
258 | return web.json_response({'error': 'User ID required'}, status=400)
259 |
260 | with open(PERMISSIONS_FILE, 'r') as f:
261 | permissions = json.load(f)
262 |
263 | user_permissions = permissions.get(user_id, {})
264 | if not user_permissions.get('editor', False):
265 | return web.json_response({'error': 'Forbidden'}, status=403)
266 |
267 | new_data = await request.json()
268 |
269 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id)
270 | if not os.path.exists(user_workflow_dir):
271 | os.makedirs(user_workflow_dir)
272 |
273 | existing_files = sorted([f for f in os.listdir(user_workflow_dir) if f.isdigit()], key=int)
274 |
275 | if len(existing_files) == 5:
276 | os.remove(os.path.join(user_workflow_dir, existing_files[0]))
277 | existing_files.pop(0)
278 |
279 | for i, file_name in enumerate(existing_files):
280 | new_name = str(i + 1)
281 | shutil.move(os.path.join(user_workflow_dir, file_name), os.path.join(user_workflow_dir, new_name))
282 |
283 | new_file_path = os.path.join(user_workflow_dir, '5')
284 | with open(new_file_path, 'w') as f:
285 | json.dump(new_data, f, indent=4)
286 |
287 | # Update the server.json file with the last saved user_id
288 | with open(SERVER_FILE, 'r+') as f:
289 | server_config = json.load(f)
290 | server_config['config']['last_saved_user_id'] = user_id
291 | f.seek(0)
292 | json.dump(server_config, f, indent=4)
293 | f.truncate()
294 |
295 | return web.json_response({'status': 'success'}, status=200)
296 |
297 | @routes.post("/nexus/workflow/backup")
298 | async def post_workflow(request):
299 |
300 | user_id = (await request.json()).get('user_id')
301 | if not user_id:
302 | return web.json_response({'error': 'User ID required'}, status=400)
303 |
304 | with open(PERMISSIONS_FILE, 'r') as f:
305 | permissions = json.load(f)
306 |
307 | user_permissions = permissions.get(user_id, {})
308 | if not user_permissions.get('editor', False):
309 | return web.json_response({'error': 'Forbidden'}, status=403)
310 |
311 | new_data = await request.json()
312 |
313 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, user_id)
314 | if not os.path.exists(user_workflow_dir):
315 | os.makedirs(user_workflow_dir)
316 |
317 | new_file_path = os.path.join(user_workflow_dir, f'lt_{round(time.time() * 1000)}')
318 | with open(new_file_path, 'w') as f:
319 | json.dump(new_data, f, indent=4)
320 |
321 | return web.json_response({'status': 'success'}, status=200)
322 |
323 | @routes.get("/nexus/workflow")
324 | async def get_latest_workflow(request):
325 | with open(SERVER_FILE, 'r') as f:
326 | server_config = json.load(f)
327 |
328 | last_saved_user_id = server_config['config'].get('last_saved_user_id')
329 | if not last_saved_user_id:
330 | return web.json_response({'error': 'No workflow saved'}, status=404)
331 |
332 | user_workflow_dir = os.path.join(WORKFLOWS_DIR, last_saved_user_id)
333 | if not os.path.exists(user_workflow_dir):
334 | return web.json_response({'error': 'User not found'}, status=404)
335 |
336 | last_file_path = os.path.join(user_workflow_dir, '5')
337 | if not os.path.exists(last_file_path):
338 | return web.json_response({'error': 'Workflow not found'}, status=404)
339 |
340 | with open(last_file_path, 'r') as f:
341 | workflow_data = json.load(f)
342 |
343 | return web.json_response(workflow_data)
344 |
345 | @routes.get("/nexus/permission/{id}")
346 | async def get_permission_by_id(request):
347 | user_id = request.match_info['id']
348 | with open(PERMISSIONS_FILE, 'r') as f:
349 | data = json.load(f)
350 | user_permission = data.get(user_id)
351 | if user_permission:
352 | return web.json_response(user_permission)
353 | else:
354 | with open(PERMISSIONS_FILE, 'r+') as f:
355 | permissions = json.load(f)
356 | permissions[user_id] = {'queue': False, 'editor': False, 'admin': False}
357 | f.seek(0)
358 | json.dump(permissions, f)
359 | f.truncate()
360 | user_permission = permissions[user_id]
361 | return web.json_response(user_permission)
362 |
363 | @routes.post("/nexus/permission/{id}")
364 | async def post_permission_by_id(request):
365 | user_id = request.match_info['id']
366 | data = await request.json()
367 | admin_id = data.get('admin_id')
368 | if not admin_id:
369 | return web.json_response({'error': 'Admin ID required'}, status=400)
370 |
371 | token = request.headers.get('Authorization')
372 | if not token:
373 | return web.json_response({'error': 'Authorization token required'}, status=400)
374 |
375 | with open(TOKENS_FILE, 'r') as f:
376 | tokens = json.load(f)
377 |
378 | if tokens.get(token) != admin_id:
379 | return web.json_response({'error': 'Invalid or expired token'}, status=403)
380 |
381 | new_permission = data.get('data')
382 | if not new_permission:
383 | return web.json_response({'error': 'Permission data required'}, status=400)
384 |
385 | with open(PERMISSIONS_FILE, 'r+') as f:
386 | permissions = json.load(f)
387 |
388 | if not permissions.get(admin_id, {}).get('admin', False):
389 | return web.json_response({'error': 'Forbidden'}, status=403)
390 |
391 | # Merge the new permissions with the existing ones
392 | existing_permissions = permissions.get(user_id, {})
393 | existing_permissions.update(new_permission)
394 |
395 | # Save the updated permissions
396 | permissions[user_id] = existing_permissions
397 | f.seek(0)
398 | json.dump(permissions, f, indent=4)
399 | f.truncate()
400 |
401 | return web.json_response({'status': 'success'}, status=200)
402 |
403 | @routes.post("/nexus/login")
404 | async def post_login(request):
405 |
406 | credentials = await request.json()
407 | user_id = credentials.get('uuid')
408 | account = credentials.get('account')
409 | password = credentials.get('password')
410 |
411 | with open(ADMINS_FILE, 'r') as f:
412 | admins = json.load(f)
413 |
414 | if admins.get(account) == password:
415 | token = str(uuid.uuid4())
416 |
417 | with open(TOKENS_FILE, 'r+') as f:
418 | tokens = json.load(f)
419 | tokens[token] = user_id
420 | f.seek(0)
421 | json.dump(tokens, f, indent=4)
422 | f.truncate()
423 |
424 | with open(PERMISSIONS_FILE, 'r+') as f:
425 | permissions = json.load(f)
426 | permissions[user_id] = {'editor': True, 'admin': True}
427 | f.seek(0)
428 | json.dump(permissions, f, indent=4)
429 | f.truncate()
430 |
431 | return web.json_response({'token': token}, status=200)
432 |
433 | else:
434 | return web.json_response({'error': 'Invalid credentials'}, status=401)
435 |
436 | @routes.post("/nexus/verify")
437 | async def post_verify(request):
438 | credentials = await request.json()
439 | token = credentials.get('token')
440 |
441 | with open(TOKENS_FILE, 'r') as f:
442 | tokens = json.load(f)
443 |
444 | user_id = tokens.get(token)
445 | if user_id:
446 | with open(PERMISSIONS_FILE, 'r') as f:
447 | permissions = json.load(f)
448 |
449 | user_permissions = permissions.get(user_id, {})
450 | return web.json_response({'user_id': user_id, 'permissions': user_permissions}, status=200)
451 | else:
452 | return web.json_response({'error': 'Invalid token'}, status=401)
453 |
454 | @routes.get("/nexus/name/{id}")
455 | async def get_name_by_id(request):
456 | user_id = request.match_info['id']
457 | with open(NAMES_FILE, 'r') as f:
458 | names = json.load(f)
459 | user_name = names.get(user_id)
460 | if user_name:
461 | return web.json_response({'name': user_name})
462 | else:
463 | return web.json_response({'error': 'User not found'}, status=404)
464 |
465 | server.app.add_routes(routes)
466 |
467 | NODE_CLASS_MAPPINGS = {}
468 | NODE_DISPLAY_NAME_MAPPINGS = {}
469 | WEB_DIRECTORY = "./web"
470 |
--------------------------------------------------------------------------------
/web/lib/nexus.litegraph.js:
--------------------------------------------------------------------------------
1 | LiteGraph.use_uuids = true
2 | LiteGraph.uuidv4 = function() { return Date.now() }
3 |
4 | LGraph.prototype.getGroupByUUID = function (uuid) {
5 | let groups = this._groups
6 | let group = null
7 | for (let index = 0; index < groups.length; index++) {
8 | let tgroup = groups[index];
9 | if (tgroup.uuid && tgroup.uuid == uuid) {
10 | group = tgroup
11 | break
12 | }
13 | }
14 | return group
15 | }
16 |
17 | LGraph.prototype.getGroupIndexByUUID = function (uuid) {
18 | let groups = this._groups
19 | let at = -1
20 | for (let index = 0; index < groups.length; index++) {
21 | let tgroup = groups[index];
22 | if (tgroup.uuid && tgroup.uuid == uuid) {
23 | at = index
24 | break
25 | }
26 | }
27 | return at
28 | }
29 |
30 |
31 | LGraph.prototype.add = function (node, skip_compute_order) {
32 |
33 | if (!node) {
34 | return;
35 | }
36 |
37 | //groups
38 | if (node.constructor === LGraphGroup) {
39 | this._groups.push(node);
40 | this.setDirtyCanvas(true);
41 | this.change();
42 | if(!node.uuid) {
43 | node.uuid = LiteGraph.uuidv4()
44 | }
45 | node.graph = this;
46 | this._version++;
47 | if (this.onGroupAdded)
48 | this.onGroupAdded(node)
49 | return;
50 | }
51 |
52 | //nodes
53 | if (node.id != -1 && this._nodes_by_id[node.id] != null) {
54 | console.warn(
55 | "Nexus LiteGraph: there is already a node with this ID, will reuse it"
56 | );
57 | if (LiteGraph.use_uuids) {
58 | node.id = LiteGraph.uuidv4()
59 | }
60 | else {
61 | node.id = ++this.last_node_id;
62 | }
63 | }
64 |
65 | if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
66 | throw "LiteGraph: max number of nodes in a graph reached";
67 | }
68 |
69 | //give him an id
70 | if (LiteGraph.use_uuids) {
71 | if (node.id == null || node.id == -1)
72 | node.id = LiteGraph.uuidv4();
73 |
74 | let uuid = localStorage.getItem('nexus-socket-uuid');
75 | if (uuid) {
76 | node.properties.owner = uuid
77 | }
78 | this.last_node_id = node.id;
79 | }
80 | else {
81 | if (node.id == null || node.id == -1) {
82 | node.id = ++this.last_node_id;
83 | } else if (this.last_node_id < node.id) {
84 | this.last_node_id = node.id;
85 | }
86 | }
87 |
88 | node.graph = this;
89 | this._version++;
90 |
91 | this._nodes.push(node);
92 | this._nodes_by_id[node.id] = node;
93 |
94 | if (node.onAdded) {
95 | node.onAdded(this);
96 | }
97 |
98 | if (this.config.align_to_grid) {
99 | node.alignToGrid();
100 | }
101 |
102 | if (!skip_compute_order) {
103 | this.updateExecutionOrder();
104 | }
105 |
106 | if (this.onNodeAdded) {
107 | this.onNodeAdded(node);
108 | }
109 |
110 | this.setDirtyCanvas(true);
111 | this.change();
112 |
113 | return node; //to chain actions
114 | };
115 |
116 | LGraph.prototype.addNexus = function (node, skip_compute_order) {
117 |
118 | if (!node) {
119 | return;
120 | }
121 |
122 | //groups
123 | if (node.constructor === LGraphGroup) {
124 | this._groups.push(node);
125 | this.setDirtyCanvas(true);
126 | this.change();
127 | // node.id = LiteGraph.uuidv4();
128 | node.graph = this;
129 | this._version++;
130 | return;
131 | }
132 |
133 | // //nodes
134 | // if (node.id != -1 && this._nodes_by_id[node.id] != null) {
135 | // console.warn(
136 | // "Nexus LiteGraph: there is already a node with this ID, will reuse it"
137 | // );
138 | // if (LiteGraph.use_uuids) {
139 | // node.id = LiteGraph.uuidv4();
140 | // }
141 | // else {
142 | // node.id = ++this.last_node_id;
143 | // }
144 | // }
145 |
146 | if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
147 | throw "LiteGraph: max number of nodes in a graph reached";
148 | }
149 |
150 | //give him an id
151 | // if (LiteGraph.use_uuids) {
152 | // if (node.id == null || node.id == -1)
153 | // let uuid = localStorage.getItem('nexus-socket-uuid');
154 | // if (uuid) {
155 | // node.properties.owner = uuid
156 | // }
157 | // node.id = LiteGraph.uuidv4();
158 | // }
159 | // else {
160 | // if (node.id == null || node.id == -1) {
161 | // node.id = ++this.last_node_id;
162 | // } else if (this.last_node_id < node.id) {
163 | this.last_node_id = node.id;
164 | // }
165 | // }
166 |
167 | node.graph = this;
168 | this._version++;
169 |
170 | this._nodes.push(node);
171 | this._nodes_by_id[node.id] = node;
172 |
173 | if (this.config.align_to_grid) {
174 | node.alignToGrid();
175 | }
176 |
177 | if (!skip_compute_order) {
178 | this.updateExecutionOrder();
179 | }
180 |
181 | if (this.onNodeAddedNexus) {
182 | this.onNodeAddedNexus(node);
183 | }
184 |
185 | this.setDirtyCanvas(true);
186 | this.change();
187 |
188 | return node; //to chain actions
189 | };
190 |
191 | LGraph.prototype.remove = function (node) {
192 | if (node.constructor === LiteGraph.LGraphGroup) {
193 | var index = this._groups.indexOf(node);
194 | if (index != -1) {
195 | this._groups.splice(index, 1);
196 | }
197 | node.graph = null;
198 | this._version++;
199 | this.setDirtyCanvas(true, true);
200 | this.change();
201 | if (this.onGroupRemoved)
202 | this.onGroupRemoved(node)
203 | return;
204 | }
205 |
206 | if (this._nodes_by_id[node.id] == null) {
207 | return;
208 | } //not found
209 |
210 | if (node.ignore_remove) {
211 | return;
212 | } //cannot be removed
213 |
214 | this.beforeChange(); //sure? - almost sure is wrong
215 |
216 | //disconnect inputs
217 | if (node.inputs) {
218 | for (var i = 0; i < node.inputs.length; i++) {
219 | var slot = node.inputs[i];
220 | if (slot.link != null) {
221 | node.disconnectInput(i);
222 | }
223 | }
224 | }
225 |
226 | //disconnect outputs
227 | if (node.outputs) {
228 | for (var i = 0; i < node.outputs.length; i++) {
229 | var slot = node.outputs[i];
230 | if (slot.links != null && slot.links.length) {
231 | node.disconnectOutput(i);
232 | }
233 | }
234 | }
235 |
236 | //node.id = -1; //why?
237 |
238 | //callback
239 | if (node.onRemoved) {
240 | node.onRemoved();
241 | }
242 |
243 | node.graph = null;
244 | this._version++;
245 |
246 | //remove from canvas render
247 | if (this.list_of_graphcanvas) {
248 | for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
249 | var canvas = this.list_of_graphcanvas[i];
250 | if (canvas.selected_nodes[node.id]) {
251 | delete canvas.selected_nodes[node.id];
252 | }
253 | if (canvas.node_dragged == node) {
254 | canvas.node_dragged = null;
255 | }
256 | }
257 | }
258 |
259 | //remove from containers
260 | var pos = this._nodes.indexOf(node);
261 | if (pos != -1) {
262 | this._nodes.splice(pos, 1);
263 | }
264 | delete this._nodes_by_id[node.id];
265 |
266 | if (this.onNodeRemoved) {
267 | this.onNodeRemoved(node);
268 | }
269 |
270 | //close panels
271 | this.sendActionToCanvas("checkPanels");
272 |
273 | this.setDirtyCanvas(true, true);
274 | this.afterChange(); //sure? - almost sure is wrong
275 | this.change();
276 |
277 | this.updateExecutionOrder();
278 | };
279 |
280 | LGraph.prototype.removeNexus = function (node) {
281 |
282 | if (node.constructor === LiteGraph.LGraphGroup) {
283 | var index = this._groups.indexOf(node);
284 | if (index != -1) {
285 | this._groups.splice(index, 1);
286 | }
287 | node.graph = null;
288 | this._version++;
289 | this.setDirtyCanvas(true, true);
290 | this.change();
291 | return;
292 | }
293 |
294 | if (this._nodes_by_id[node.id] == null) {
295 | return;
296 | } //not found
297 |
298 | if (node.ignore_remove) {
299 | return;
300 | } //cannot be removed
301 |
302 | this.beforeChange(); //sure? - almost sure is wrong
303 |
304 | //disconnect inputs
305 | if (node.inputs) {
306 | for (var i = 0; i < node.inputs.length; i++) {
307 | var slot = node.inputs[i];
308 | if (slot.link != null) {
309 | node.disconnectInput(i);
310 | }
311 | }
312 | }
313 |
314 | //disconnect outputs
315 | if (node.outputs) {
316 | for (var i = 0; i < node.outputs.length; i++) {
317 | var slot = node.outputs[i];
318 | if (slot.links != null && slot.links.length) {
319 | node.disconnectOutput(i);
320 | }
321 | }
322 | }
323 |
324 | //node.id = -1; //why?
325 |
326 | //callback
327 | if (node.onRemoved) {
328 | node.onRemoved();
329 | }
330 |
331 | node.graph = null;
332 | this._version++;
333 |
334 | //remove from canvas render
335 | if (this.list_of_graphcanvas) {
336 | for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
337 | var canvas = this.list_of_graphcanvas[i];
338 | if (canvas.selected_nodes[node.id]) {
339 | delete canvas.selected_nodes[node.id];
340 | }
341 | if (canvas.node_dragged == node) {
342 | canvas.node_dragged = null;
343 | }
344 | }
345 | }
346 |
347 | //remove from containers
348 | var pos = this._nodes.indexOf(node);
349 | if (pos != -1) {
350 | this._nodes.splice(pos, 1);
351 | }
352 | delete this._nodes_by_id[node.id];
353 |
354 | //close panels
355 | this.sendActionToCanvas("checkPanels");
356 |
357 | this.setDirtyCanvas(true, true);
358 | this.afterChange(); //sure? - almost sure is wrong
359 | this.change();
360 |
361 | this.updateExecutionOrder();
362 | };
363 |
364 | LGraphNode.prototype.configureNexus = function (info) {
365 | if (this.graph) {
366 | this.graph._version++;
367 | }
368 | for (var j in info) {
369 | if (j == "properties") {
370 | //i don't want to clone properties, I want to reuse the old container
371 | for (var k in info.properties) {
372 | this.properties[k] = info.properties[k];
373 | }
374 | continue;
375 | }
376 |
377 | if (info[j] == null) {
378 | continue;
379 | } else if (typeof info[j] == "object") {
380 | //object
381 | if (this[j] && this[j].configure) {
382 | this[j].configure(info[j]);
383 | } else {
384 | this[j] = LiteGraph.cloneObject(info[j], this[j]);
385 | }
386 | } //value
387 | else {
388 | this[j] = info[j];
389 | }
390 | }
391 |
392 | if (!info.title) {
393 | this.title = this.constructor.title;
394 | }
395 |
396 | if (this.inputs) {
397 | for (var i = 0; i < this.inputs.length; ++i) {
398 | var input = this.inputs[i];
399 | if (this.onInputAdded)
400 | this.onInputAdded(input);
401 | }
402 | }
403 |
404 | if (this.outputs) {
405 | for (var i = 0; i < this.outputs.length; ++i) {
406 | var output = this.outputs[i];
407 | if (!output.links) {
408 | continue;
409 | }
410 | if (this.onOutputAdded)
411 | this.onOutputAdded(output);
412 | }
413 | }
414 |
415 | if (this.widgets) {
416 | for (var i = 0; i < this.widgets.length; ++i) {
417 | var w = this.widgets[i];
418 | if (!w)
419 | continue;
420 | if (w.options && w.options.property && (this.properties[w.options.property] != undefined))
421 | w.value = JSON.parse(JSON.stringify(this.properties[w.options.property]));
422 | }
423 | if (info.widgets_values) {
424 | for (var i = 0; i < info.widgets_values.length; ++i) {
425 | if (this.widgets[i]) {
426 | this.widgets[i].value = info.widgets_values[i];
427 | }
428 | }
429 | }
430 | }
431 |
432 | if (this.onConfigure) {
433 | this.onConfigure(info);
434 | }
435 | };
436 |
437 | LGraphCanvas.prototype.selectNodesNexus = function (nodes, add_to_current_selection) {
438 | if (!add_to_current_selection) {
439 | this.deselectAllNodes();
440 | }
441 |
442 | nodes = nodes || this.graph._nodes;
443 | if (typeof nodes == "string") nodes = [nodes];
444 | for (var i in nodes) {
445 | var node = nodes[i];
446 | if (node.is_selected) {
447 | this.deselectNode(node);
448 | continue;
449 | }
450 |
451 | if (!node.is_selected && node.onSelected) {
452 | node.onSelected();
453 | }
454 | node.is_selected = true;
455 | this.selected_nodes[node.id] = node;
456 |
457 | if (node.inputs) {
458 | for (var j = 0; j < node.inputs.length; ++j) {
459 | this.highlighted_links[node.inputs[j].link] = true;
460 | }
461 | }
462 | if (node.outputs) {
463 | for (var j = 0; j < node.outputs.length; ++j) {
464 | var out = node.outputs[j];
465 | if (out.links) {
466 | for (var k = 0; k < out.links.length; ++k) {
467 | this.highlighted_links[out.links[k]] = true;
468 | }
469 | }
470 | }
471 | }
472 | }
473 |
474 | this.setDirty(true);
475 | };
476 |
477 | LGraphCanvas.prototype.deselectNodeNexus = function(node) {
478 |
479 | if (!node.is_selected) {
480 | return;
481 | }
482 |
483 | if (node.onDeselected) {
484 | node.onDeselected();
485 | }
486 |
487 | node.is_selected = false;
488 |
489 | //remove highlighted
490 | if (node.inputs) {
491 | for (var i = 0; i < node.inputs.length; ++i) {
492 | delete this.highlighted_links[node.inputs[i].link];
493 | }
494 | }
495 | if (node.outputs) {
496 | for (var i = 0; i < node.outputs.length; ++i) {
497 | var out = node.outputs[i];
498 | if (out.links) {
499 | for (var j = 0; j < out.links.length; ++j) {
500 | delete this.highlighted_links[out.links[j]];
501 | }
502 | }
503 | }
504 | }
505 | };
506 |
507 | LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
508 | this._pos[0] += deltax;
509 | this._pos[1] += deltay;
510 | if (ignore_nodes) {
511 | return;
512 | }
513 | for (var i = 0; i < this._nodes.length; ++i) {
514 | var node = this._nodes[i];
515 | node.pos[0] += deltax;
516 | node.pos[1] += deltay;
517 | if (this.onGroupNodeMoved)
518 | this.onGroupNodeMoved(node)
519 | }
520 | if (this.onGroupMoved)
521 | this.onGroupMoved(this)
522 | };
523 |
524 | LGraphGroup.prototype.configure = function (o) {
525 | this.title = o.title;
526 | this.owner = o.owner;
527 | this.uuid = o.uuid;
528 | this._bounding.set(o.bounding);
529 | this.color = o.color;
530 | if (o.font_size) {
531 | this.font_size = o.font_size;
532 | }
533 | };
534 |
535 | LGraphGroup.prototype.serialize = function () {
536 | var b = this._bounding;
537 | return {
538 | title: this.title,
539 | bounding: [
540 | Math.round(b[0]),
541 | Math.round(b[1]),
542 | Math.round(b[2]),
543 | Math.round(b[3])
544 | ],
545 | uuid: this.uuid,
546 | color: this.color,
547 | font_size: this.font_size
548 | };
549 | };
550 |
551 | LGraph.prototype.addNode = function (nodeData) {
552 | let node = LiteGraph.createNode(nodeData.type, nodeData.title);
553 | if (!node) {
554 | console.warn("Node type not found: " + nodeData.type);
555 | return false;
556 | }
557 | node.id = nodeData.id;
558 | node.configureNexus(nodeData);
559 | this.addNexus(node);
560 | return true;
561 | };
562 |
563 | LGraph.prototype.removeNode = function (data) {
564 | let node = this.getNodeById(data.id);
565 | if (!node) {
566 | console.warn("Node ID not found: " + data);
567 | return false;
568 | }
569 | this.removeNexus(node);
570 | return true;
571 | };
572 |
573 | LGraph.prototype.addLink = function (linkData) {
574 | let link = new LiteGraph.LLink();
575 | link.configure(linkData);
576 | this.links[link.id] = link;
577 | return;
578 | };
579 |
580 | LGraph.prototype.addGroup = function (data) {
581 | var group = new LiteGraph.LGraphGroup();
582 | group.configure(data);
583 | this._groups.push(group);
584 | this.setDirtyCanvas(true);
585 | this.change();
586 | group.graph = this;
587 | this._version++;
588 | if (this.onGroupAdded)
589 | this.onGroupAdded(group)
590 | return;
591 | };
592 |
593 | LGraph.prototype.removeGroup = function (group) {
594 | let existingGroupIndex = this.getGroupIndexByUUID(group.uuid);
595 | if (existingGroupIndex != -1) {
596 | this._groups.splice(existingGroupIndex, 1);
597 | }
598 | group.graph = null;
599 | this._version++;
600 | this.setDirtyCanvas(true, true);
601 | this.change();
602 | return;
603 | };
604 |
605 | LGraph.prototype.updateNode = function (nodeData, type = "all") {
606 | let existingNode = this.getNodeById(nodeData.id);
607 | if (existingNode) {
608 | if (type == "all") {
609 | existingNode.configureNexus(nodeData);
610 | } else if (type == "widgets") {
611 | if (existingNode.widgets) {
612 | for (var i = 0; i < existingNode.widgets.length; ++i) {
613 | var w = existingNode.widgets[i];
614 | if (!w)
615 | continue;
616 | if (w.options && w.options.property && (existingNode.properties[w.options.property] != undefined))
617 | w.value = JSON.parse(JSON.stringify(existingNode.properties[w.options.property]));
618 | }
619 | if (nodeData.widgets_values) {
620 | for (var i = 0; i < nodeData.widgets_values.length; ++i) {
621 | if (existingNode.widgets[i]) {
622 | existingNode.widgets[i].value = nodeData.widgets_values[i];
623 | }
624 | }
625 | }
626 | }
627 | }
628 | } else {
629 | this.addNode(nodeData);
630 | }
631 | };
632 |
633 | LGraph.prototype.updateLink = function (linkData) {
634 |
635 | if (linkData) {
636 |
637 | let id
638 |
639 | if (linkData instanceof Object) {
640 | id = linkData.id
641 | } else {
642 | id = linkData[0]
643 | }
644 |
645 | let existingLink = this.links[id];
646 | if (existingLink) {
647 | existingLink.configure(linkData);
648 | } else {
649 | this.addLink(linkData);
650 | }
651 |
652 | }
653 |
654 | };
655 |
656 | LGraph.prototype.updateLinkManual = function (change, linkData, node, graph) {
657 | if (change == "add") {
658 | this.updateLink(linkData)
659 | } else if (change == "remove") {
660 | let existingLink = this.links[linkData[0]];
661 | if (existingLink) {
662 | this.removeLink(linkData[0])
663 | delete this.links[linkData[0]]
664 | }
665 | }
666 | if (node) {
667 | this.updateNode(node)
668 | }
669 | graph.updateExecutionOrder();
670 | graph._version++;
671 | graph.setDirtyCanvas(true, true);
672 | };
673 |
674 | LGraph.prototype.updateGroup = function (groupData) {
675 | let existingGroup = this.getGroupByUUID(groupData.uuid);
676 | if (existingGroup) {
677 | existingGroup.configure(groupData);
678 | } else {
679 | this.addGroup(groupData);
680 | }
681 | };
682 |
683 | LGraph.prototype.updateNodes = function (newNodes) {
684 | newNodes.forEach(nodeData => this.updateNode(nodeData));
685 | };
686 |
687 | LGraph.prototype.updateLinks = function (newLinks) {
688 | newLinks.forEach(linkData => this.updateLink(linkData));
689 | };
690 |
691 | LGraph.prototype.updateGroups = function (newGroups) {
692 | newGroups.forEach(groupData => this.updateGroup(groupData));
693 | };
--------------------------------------------------------------------------------
/web/lib/nexus.cmd.js:
--------------------------------------------------------------------------------
1 | import { nexusLogin, nexusVerify, getPermissionById, postPermissionById, getUserSpecificWorkflowsMeta, getUserSpecificWorkflow } from "./nexus.api.js"
2 | import { addChatWindowMessage, inputMain, chatWindow } from "./nexus.chat.js"
3 | import { UserPanel } from "./nexus.panel.js"
4 | import { WorkflowPanel } from "./nexus.workflow.panel.js"
5 |
6 | export class NexusCommands {
7 |
8 | constructor(app, api, nexusSocket, color, mouseManger, workflowManager, promptControl) {
9 |
10 | this.color = color
11 |
12 | this.colors = {
13 | "join": "#ff6e4d",
14 | "info": "#fba510",
15 | "personal": "#fba510",
16 | "name": "#00fa00",
17 | "none": "#ffffff"
18 | }
19 |
20 | this.inputMain = inputMain
21 | this.nexusSocket = nexusSocket
22 |
23 | this.app = app
24 | this.api = api
25 |
26 | this.workflowManager = workflowManager
27 | this.promptControl = promptControl
28 | this.mouseManger = mouseManger
29 |
30 | this.nexusSocket.onConnected.push(this.handleConnect.bind(this))
31 | this.nexusSocket.onMessageReceive.push(this.handleMessageReceive.bind(this))
32 |
33 | this.inputMain.addEventListener('message', this.handleMessage.bind(this))
34 |
35 | document.addEventListener('keydown', this.handleChatWindow.bind(this));
36 |
37 | this.disableComfyThings = this.disableComfyThings.bind(this);
38 | this.enableComfyThings = this.enableComfyThings.bind(this);
39 | this.disableGraphSelectionWithExtremePrejudice = this.disableGraphSelectionWithExtremePrejudice.bind(this);
40 |
41 | this.comfyThings = [
42 | '#extraOptions',
43 | '#queue-button',
44 | '#comfy-save-button',
45 | '#comfy-file-input',
46 | '#comfy-clear-button',
47 | '#comfy-load-default-button',
48 | '.queue-tab-button.side-bar-button',
49 | '.queue-tab-button.side-bar-button',
50 | '#comfy-refresh-button',
51 | '#queue-front-button',
52 | '#comfy-view-queue-button',
53 | '#comfy-view-history-button'
54 | ];
55 |
56 | chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'none')
57 | chatWindow.show()
58 |
59 | setInterval((chatWindow) => {
60 | chatWindow.childrens[0].childrens[1].el.textContent = Object.keys(this.nexusSocket.userManager.users).length
61 | }, 1000, chatWindow);
62 |
63 | this.up = new UserPanel()
64 | this.up.onCommandReceived = this.onPanelCommandReceived.bind(this)
65 |
66 | this.wp = new WorkflowPanel()
67 | this.wp.onCommandReceived = this.onWFPanelCommandReceived.bind(this)
68 |
69 | this.spectating = null
70 | this.lastkey = null
71 |
72 | }
73 |
74 | timeAgo(timeString) {
75 | const now = new Date(); // current time
76 | const time = new Date(timeString); // input time
77 |
78 | const diff = Math.floor((now - time) / 1000); // difference in seconds
79 |
80 | const minutes = Math.floor(diff / 60);
81 | const hours = Math.floor(minutes / 60);
82 | const days = Math.floor(hours / 24);
83 |
84 | if (minutes < 1) return "just now";
85 | if (minutes === 1) return "1 minute ago";
86 | if (minutes < 60) return `${minutes} minutes ago`;
87 | if (hours === 1) return "1 hour ago";
88 | if (hours < 24) return `${hours} hours ago`;
89 | if (days === 1) return "1 day ago";
90 |
91 | return `${days} days ago`;
92 | }
93 |
94 | async verifyLogin() {
95 |
96 | this.disableComfyThings()
97 |
98 | let token = localStorage.getItem('nexus-socket-token');
99 | if (token) {
100 | let user = await nexusVerify(token)
101 | if (user && this.nexusSocket.uuid == user.user_id) {
102 | this.enableComfyThings()
103 | this.promptControl.handleControl(true)
104 | this.promptControl.handleEditor(true)
105 | this.workflowManager.handleWorkflowEvents(true)
106 | this.nexusSocket.admin = true
107 | addChatWindowMessage("", "You are now an admin!", this.color.info, this.colors.personal, true)
108 | this.workflowManager.saveManager(60)
109 | }
110 | else {
111 | this.promptControl.handleControl(false)
112 | this.promptControl.handleEditor(false)
113 | this.workflowManager.handleWorkflowEvents(false)
114 | this.workflowManager.removeSaveManager()
115 | addChatWindowMessage("", "You are a viewer!", this.color.info, this.colors.info, true)
116 | }
117 | }
118 | else {
119 |
120 | let permissions = await getPermissionById(this.nexusSocket.uuid)
121 |
122 | if (permissions) {
123 |
124 | let editor = permissions.editor ? permissions.editor : false
125 | let queue = permissions.queue ? permissions.queue : false
126 |
127 | this.workflowManager.handleWorkflowEvents(editor)
128 |
129 | this.promptControl.handleEditor(editor)
130 | this.promptControl.handleControl(queue)
131 |
132 | this.nexusSocket.queue = queue
133 | this.nexusSocket.editor = editor
134 |
135 | if (editor) {
136 | this.workflowManager.saveManager(60)
137 | }
138 | else {
139 | this.workflowManager.removeSaveManager()
140 | }
141 |
142 | addChatWindowMessage("", `Permission: Workflow Editor ${editor ? "Yes" : "No"} | Queue Prompt ${queue ? "Yes" : "No"} `, this.color.info, this.colors.personal, true)
143 |
144 | if (queue) {
145 | addChatWindowMessage("", `You can now queue prompt press CTRL+Enter`, this.color.info, this.colors.personal, true)
146 | }
147 |
148 | }
149 |
150 | }
151 |
152 | this.workflowManager.loadLatestWorkflow()
153 | }
154 |
155 | handleConnect() {
156 |
157 | let name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name")
158 | this.nexusSocket.sendMessage('chat-join', { name }, false)
159 | this.verifyLogin()
160 |
161 | }
162 |
163 | findChildrenAfterHr(parentClass) {
164 | const parentDiv = document.querySelector(`.${parentClass}`);
165 | const childElementsAfterHr = [];
166 | if (parentDiv) {
167 | const hr = parentDiv.querySelector('hr');
168 | if (hr) {
169 | let sibling = hr.nextElementSibling;
170 | while (sibling) {
171 | childElementsAfterHr.push(sibling);
172 | sibling = sibling.nextElementSibling;
173 | }
174 | }
175 | }
176 | return childElementsAfterHr;
177 | }
178 |
179 | disableGraphSelectionWithExtremePrejudice() {
180 | if (!this.nexusSocket.admin && !this.nexusSocket.editor) {
181 | if (this.app.graph && this.app.graph.list_of_graphcanvas && this.app.graph.list_of_graphcanvas.length > 0) {
182 | this.app.graph.list_of_graphcanvas[0].deselectAllNodes()
183 | }
184 | }
185 | }
186 |
187 | disableComfyThings() {
188 |
189 | this.comfyThings.forEach((selector) => {
190 | const elements = document.querySelectorAll(selector);
191 | elements.forEach((element) => {
192 | element.disabled = true;
193 | element.style.pointerEvents = 'none';
194 | });
195 | });
196 |
197 | document.querySelectorAll('.comfy-settings-btn').forEach(button => {
198 | button.disabled = true;
199 | button.style.pointerEvents = 'none';
200 | });
201 |
202 | const children = this.findChildrenAfterHr('comfy-menu');
203 |
204 | children.forEach(element => {
205 | if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
206 | element.disabled = true;
207 | }
208 | });
209 |
210 | document.querySelector(`.comfy-menu`).style.display = 'none';
211 | document.querySelector(`.comfy-menu`).style.pointerEvents = 'none';
212 |
213 |
214 | clearInterval(this.disableComfyThingsTimer)
215 |
216 | this.disableComfyThingsTimer = setInterval(() => {
217 |
218 |
219 | this.comfyThings.forEach((selector) => {
220 | const elements = document.querySelectorAll(selector);
221 | elements.forEach((element) => {
222 | element.disabled = true;
223 | element.style.pointerEvents = 'none';
224 | });
225 | });
226 |
227 | document.querySelectorAll('.comfy-settings-btn').forEach(button => {
228 | button.disabled = true;
229 | button.style.pointerEvents = 'none';
230 | });
231 |
232 | const children = this.findChildrenAfterHr('comfy-menu');
233 |
234 | children.forEach(element => {
235 | if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
236 | element.disabled = true;
237 | }
238 | });
239 |
240 | document.querySelector(`.comfy-menu`).style.display = 'none';
241 | document.querySelector(`.comfy-menu`).style.pointerEvents = 'none';
242 |
243 | this.promptControl.handleEditor(this.nexusSocket.editor ? this.nexusSocket.editor : false)
244 | this.promptControl.handleControl(this.nexusSocket.queue ? this.nexusSocket.queue : false)
245 |
246 | }, 500);
247 |
248 | this.disableGraphSelectionWithExtremePrejudiceTimer = setInterval(this.disableGraphSelectionWithExtremePrejudice, 50)
249 |
250 | }
251 |
252 | enableComfyThings() {
253 |
254 | clearInterval(this.disableComfyThingsTimer)
255 | clearInterval(this.disableGraphSelectionWithExtremePrejudiceTimer)
256 |
257 | this.comfyThings.forEach((selector) => {
258 | const elements = document.querySelectorAll(selector);
259 | elements.forEach((element) => {
260 | element.disabled = false;
261 | element.style.pointerEvents = 'all';
262 | });
263 | });
264 |
265 | document.querySelectorAll('.comfy-settings-btn').forEach(button => {
266 | button.disabled = false;
267 | button.style.pointerEvents = 'all';
268 | });
269 |
270 | const children = this.findChildrenAfterHr('comfy-menu');
271 |
272 | children.forEach(element => {
273 | if (element.tagName === 'BUTTON' || element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
274 | element.disabled = false;
275 | }
276 | });
277 |
278 | document.querySelector(`.comfy-menu`).style.display = 'flex';
279 | document.querySelector(`.comfy-menu`).style.pointerEvents = 'all';
280 |
281 | }
282 |
283 |
284 | handleChatWindow(event) {
285 |
286 | let key = event.key
287 | let label = inputMain.childrens[0]
288 | let input = inputMain.childrens[1]
289 |
290 | let active = {
291 | tag: document.activeElement.tagName,
292 | el: document.activeElement,
293 | chat: document.activeElement == input.el
294 | }
295 |
296 | function activateChat() {
297 | let allowedTags = ['INPUT', 'TEXTAREA']
298 | if (!allowedTags.includes(active.tag) && input.visible == false) {
299 | event.preventDefault();
300 | label.show()
301 | input.show()
302 | input.el.focus()
303 | input.el.value = ""
304 | input.focusTimer = setInterval((parent, input) => {
305 | if (document.activeElement != input && parent.visible == true)
306 | input.focus()
307 | }, 500, input, input.el);
308 | chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'all')
309 | }
310 | }
311 |
312 | function deactivateChat() {
313 | if (!active.chat) return;
314 | label.hide()
315 | input.el.blur();
316 | input.el.value = ""
317 | input.hide()
318 | clearTimeout(input.focusTimer)
319 | chatWindow.childrens[3].softUpdate('#nexus-chat-window', 'pointer-events', 'none')
320 | }
321 |
322 | function sendMessage() {
323 | if (!active.chat) return;
324 | let message = input.el.value
325 | inputMain.dispatchEvent(new CustomEvent('message', { detail: { message } }))
326 | deactivateChat()
327 | }
328 |
329 | switch (key) {
330 | case 't':
331 | case 'T':
332 | activateChat()
333 | this.togglePlayerTab(true)
334 | this.toggleWFTab(true)
335 | break;
336 | case 'Escape':
337 | deactivateChat()
338 | break;
339 | case 'Enter':
340 | sendMessage()
341 | break;
342 | case 'p':
343 | case 'P':
344 | if (event.altKey && event.shiftKey) {
345 | if (!active.chat)
346 | this.togglePlayerTab()
347 | }
348 | break;
349 | case 'o':
350 | case 'O':
351 | if (event.altKey && event.shiftKey) {
352 | if (!active.chat)
353 | this.toggleWFTab()
354 | }
355 | break;
356 |
357 | }
358 |
359 | return active
360 | }
361 |
362 | toggleWFTab(forceHide, forceReload) {
363 | if (this.nexusSocket.admin || this.nexusSocket.editor) {
364 |
365 | if (forceHide) {
366 | this.wp.active = 0
367 | this.wp.panel.hide()
368 | } else if (forceReload) {
369 | this.prepareWFPanel()
370 | }
371 | else {
372 | if (this.wp.active == 0) {
373 | this.wp.active = 1
374 | this.wp.panel.show()
375 | this.prepareWFPanel()
376 | } else if (this.wp.active == 1) {
377 | this.wp.active = 0
378 | this.wp.panel.hide()
379 | }
380 | }
381 |
382 | if (this.wp.active == 1) {
383 | this.workflowManager.removeSaveManager()
384 | }
385 | else {
386 | this.workflowManager.saveManager(this.workflowManager.saveTime || 60)
387 | }
388 |
389 | }
390 | }
391 |
392 | togglePlayerTab(forceHide, forceReload) {
393 |
394 | if (forceHide) {
395 | this.up.active = 0
396 | this.up.panel.hide()
397 | } else if (forceReload) {
398 | this.preparePanel(this.nexusSocket.admin)
399 | } else {
400 | if (this.up.active == 0) {
401 | this.up.active = 1
402 | this.up.panel.show()
403 | this.preparePanel(this.nexusSocket.admin)
404 | } else if (this.up.active == 1) {
405 | this.up.active = 0
406 | this.up.panel.hide()
407 | }
408 | }
409 |
410 | }
411 |
412 | async onWFPanelCommandReceived(kind, label, data, active) {
413 |
414 | switch (kind) {
415 | case 'button':
416 | if (label == "load") {
417 | let wfid = data // workflowid is data
418 | let wf = await getUserSpecificWorkflow(this.nexusSocket.uuid, wfid)
419 | if (wf) {
420 | wf.clear = true
421 | let message = {
422 | name: "workflow",
423 | from: this.nexusSocket.uuid,
424 | data: { workflow: wf }
425 | }
426 | this.nexusSocket.onMessageReceive.forEach(f => { f(message); });
427 | if (this.nexusSocket.admin) {
428 | this.nexusSocket.sendMessage('workflow', { workflow: wf }, false)
429 | }
430 | }
431 | }
432 | break;
433 | }
434 |
435 | }
436 |
437 | async onPanelCommandReceived(kind, label, data, active) {
438 |
439 | switch (kind) {
440 | case 'button':
441 | if (label == "spectate") {
442 | this.nexusSocket.sendMessage("spectate", { on: active, receiver: data }, false)
443 | if (active == 1) {
444 | this.spectating = data // userid is data
445 | }
446 | else {
447 | this.spectating = null
448 | }
449 | } else if (label == "editor") {
450 | if (this.nexusSocket.admin) {
451 | let token = localStorage.getItem('nexus-socket-token');
452 | await postPermissionById(data, this.nexusSocket.uuid, { editor: active ? true : false }, token) // userid is data
453 | this.nexusSocket.sendMessage("permission", { receiver: data }, false)
454 | }
455 | } else if (label == "queue") {
456 | if (this.nexusSocket.admin) {
457 | let token = localStorage.getItem('nexus-socket-token');
458 | await postPermissionById(data, this.nexusSocket.uuid, { queue: active ? true : false }, token) // userid is data
459 | this.nexusSocket.sendMessage("permission", { receiver: data }, false)
460 | }
461 | }
462 | else if (label == "mouse") {
463 | this.nexusSocket.sendMessage("mouse_view_toggle", { on: active, receiver: data }, false)
464 | this.nexusSocket.userManager.set(data, 'hide_mouse', active)
465 | }
466 | break;
467 | }
468 |
469 | }
470 |
471 | async prepareWFPanel() {
472 |
473 | const columns = ['Name', 'Created', 'Action'];
474 |
475 | this.wp.addColumnsToPanel(columns);
476 |
477 | let meta = await getUserSpecificWorkflowsMeta(this.nexusSocket.uuid)
478 |
479 | const rows = [];
480 | const callbacks = [];
481 | const values = [];
482 | const data = [];
483 |
484 | for (let id = 0; id < meta.length; id++) {
485 |
486 | const file = meta[id];
487 |
488 | let fn = file.file_name + ""
489 |
490 | let name = (fn.includes('lt') ? 'Long Term ' : 'Short Term ')
491 | // let time = file.last_saved.split(':')
492 | // time = time.join("|")
493 |
494 | // console.log(time)
495 | let time = this.timeAgo(file.last_saved)
496 |
497 | rows.push([name, time, 'button:load'])
498 | callbacks.push([null, null, this.wp.onclick])
499 | values.push([null, null, '0'])
500 | data.push([null, null, file.file_name])
501 |
502 | }
503 |
504 | this.wp.addRowsToPanel(rows, callbacks, values, data);
505 | }
506 |
507 | async preparePanel(admin) {
508 |
509 | const columns = ['Name', 'Actions', 'Online'];
510 | this.up.addColumnsToPanel(columns);
511 |
512 | const users = this.nexusSocket.userManager.users;
513 | const usersIds = Object.keys(users);
514 |
515 | const rows = [];
516 | const callbacks = [];
517 | const values = [];
518 | const data = [];
519 |
520 | for (const id of usersIds) {
521 |
522 | const user = users[id];
523 | const name = id === this.nexusSocket.uuid ? `${user.name} (You)` : user.name;
524 | const self = id === this.nexusSocket.uuid;
525 |
526 | const status = user.status === "online" || self ? '🟢' : '🔴';
527 | const hide_mouse = user.hide_mouse ? 0 : 1;
528 | const permissions = await getPermissionById(id);
529 | const spectating = id == this.spectating ? 1 : 0
530 |
531 | callbacks.push([null, self ? 'none' : this.up.onclick, null]);
532 |
533 | let powers = 'button:spectate,button:editor,button:queue,button:mouse'
534 |
535 | if (!admin) {
536 | powers = 'button:spectate,button:mouse'
537 | }
538 |
539 | rows.push([name, self ? 'button:none' : powers, status]);
540 |
541 | let row_values = `${spectating},${permissions.editor ? 1 : 0},${permissions.queue ? 1 : 0},${hide_mouse}`
542 | if (!admin) {
543 | row_values = `${spectating},${hide_mouse}`
544 | }
545 |
546 | values.push([null, row_values, null]);
547 |
548 | let row_data = `${id},${id},${id},${id}`
549 | if (!admin) {
550 | row_data = `${id},${id}`
551 | }
552 |
553 | data.push([null, row_data, null]);
554 |
555 | }
556 |
557 | this.up.addRowsToPanel(rows, callbacks, values, data);
558 | }
559 |
560 | handleMessageReceive(message) {
561 |
562 | let name = message.name;
563 | let from = message.from;
564 | let data = message.data
565 |
566 | // console.log(name, data, from)
567 |
568 | if (name == "chat") {
569 |
570 | let user_name = this.nexusSocket.userManager.get(from, "name") || "User";
571 | let mouse = this.nexusSocket.userManager.get(from, "mouse") || "#0f0";
572 | let message = data.message
573 | addChatWindowMessage(user_name, message, mouse[3], this.colors.none)
574 |
575 | } else if (name == "nick") {
576 |
577 | let old_name = data.old_name
578 | let new_name = data.new_name
579 |
580 | addChatWindowMessage(old_name + " is now known as ", new_name, this.colors.personal, this.colors.personal, true)
581 | this.nexusSocket.userManager.set(from, "name", new_name)
582 |
583 | } else if (name == "chat-join") {
584 |
585 | let name = data.name
586 | addChatWindowMessage(name + " joined the server", "", this.colors.personal, this.colors.personal, true)
587 | this.nexusSocket.userManager.set(from, "name", name)
588 |
589 | name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name")
590 | this.nexusSocket.sendMessage('name', { name }, false)
591 |
592 | if (this.up.active) {
593 | this.togglePlayerTab(false, true)
594 | }
595 |
596 | } else if (name == "name") {
597 |
598 | let name = data.name
599 | this.nexusSocket.userManager.set(from, "name", name)
600 | //console.log("Received name:", name)
601 |
602 | if (this.up.active) {
603 | this.togglePlayerTab(false, true)
604 | }
605 |
606 | } else if (name == 'afk' || name == 'online') {
607 |
608 | this.nexusSocket.userManager.set(from, "status", name)
609 |
610 | if (this.up.active) {
611 | this.togglePlayerTab(false, true)
612 | }
613 |
614 | } else if (name == 'permission') {
615 |
616 |
617 | // window.location.reload()
618 |
619 | this.verifyLogin()
620 |
621 | if (this.up.active) {
622 | this.togglePlayerTab(false, true)
623 | }
624 |
625 | }
626 |
627 | }
628 |
629 | async handleMessage(evt) {
630 |
631 | let message = evt.detail.message
632 | message = this.cleanMessage(message)
633 |
634 | if (message) {
635 |
636 | let startsWithCommand = this.startsWithCommand(message)
637 |
638 | if (startsWithCommand) {
639 | let parts = this.extractCommandAndValue(message)
640 | let cmds = { 'nick': true, 'login': true, 'logout': true }
641 | let cmd = parts.command
642 | let values = parts.values
643 |
644 | if (cmds[cmd]) {
645 | switch (cmd) {
646 | case 'nick':
647 | let new_name = this.cleanMessage(values[0], 16)
648 | let old_name = this.nexusSocket.userManager.get(this.nexusSocket.uuid, "name") || "User"
649 | if (!new_name) {
650 | addChatWindowMessage("", "Name is invalid", this.colors.info, this.colors.info, true)
651 | }
652 | else {
653 | localStorage.setItem('nexus-socket-name', new_name);
654 | this.nexusSocket.sendMessage('nick', { old_name, new_name })
655 | }
656 | break;
657 | case 'login':
658 |
659 | let account = this.cleanMessage(values[0], 16)
660 | let password = this.cleanMessage(values[1], 16)
661 | let uuid = this.nexusSocket.uuid
662 | let token = await nexusLogin(uuid, account, password)
663 | if (token) {
664 | localStorage.setItem('nexus-socket-token', token.token);
665 | // addChatWindowMessage("", "You are now an admin!", this.color.info, this.colors.info, true)
666 | this.verifyLogin()
667 | // window.location.reload()
668 | }
669 | else {
670 | addChatWindowMessage("", "Admin account/password invalid!", this.color.info, this.colors.info, true)
671 | }
672 | break;
673 | case 'logout':
674 | let usertoken = localStorage.getItem('nexus-socket-token')
675 | if (this.nexusSocket.admin) {
676 | await postPermissionById(this.nexusSocket.uuid, this.nexusSocket.uuid, { admin: false, queue: false, editor: false }, usertoken)
677 | localStorage.removeItem('nexus-socket-token');
678 | window.location.reload()
679 | }
680 | break;
681 |
682 | default:
683 | break;
684 | }
685 | }
686 |
687 | }
688 | else {
689 | addChatWindowMessage("You", message, this.color, this.colors.none)
690 | this.nexusSocket.sendMessage('chat', { message }, false)
691 | }
692 |
693 | }
694 |
695 | }
696 |
697 | cleanMessage(message, length = 512) {
698 | message = message.substr(0, length);
699 | if (message.length === 0) {
700 | return null;
701 | } else {
702 | let msg = "";
703 | const nonAsciiRe = /[^\x00-\x7F]/;
704 | const emojiRe = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]/u;
705 | const htmlTagRe = /<\/?[^>]+(>|$)/g;
706 | message = message.replace(htmlTagRe, '');
707 | for (let char of message) {
708 | if (!(nonAsciiRe.test(char) && !emojiRe.test(char))) {
709 | msg += char;
710 | }
711 | }
712 | return msg;
713 | }
714 | }
715 |
716 | startsWithCommand(str) {
717 | const commandPattern = /^\/\w+(\s+\S+)?/;
718 | return commandPattern.test(str);
719 | }
720 |
721 | extractCommandAndValue(str) {
722 | const commandPattern = /^\/(\w+)\s*(.*)/;
723 | const match = str.match(commandPattern);
724 | if (match) {
725 | const command = match[1];
726 | const values = match[2].trim() ? match[2].split(/\s+/) : [];
727 | return {
728 | command,
729 | values
730 | };
731 | } else {
732 | return null;
733 | }
734 | }
735 |
736 |
737 | }
--------------------------------------------------------------------------------