Refine worker UI theme behavior and remote fallback add flow
This commit is contained in:
+9
-6
@@ -133,8 +133,9 @@ export const UI_STYLES = {
|
||||
statusDot: "display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 10px;",
|
||||
controlsDiv: "padding: 0 12px 12px 12px; display: flex; gap: 6px;",
|
||||
formGroup: "display: flex; flex-direction: column; gap: 5px;",
|
||||
formLabel: "font-size: 12px; color: #ccc; font-weight: 500;",
|
||||
formInput: "padding: 6px 10px; color: white; font-size: 12px; transition: border-color 0.2s;",
|
||||
formLabel: "font-size: 12px; color: var(--dist-label-text, #ccc); font-weight: 500;",
|
||||
formInput:
|
||||
"padding: 6px 10px; color: var(--dist-input-text, white); background: var(--dist-input-bg, transparent); font-size: 12px; transition: border-color 0.2s;",
|
||||
|
||||
// Card styles
|
||||
cardBase: "margin-bottom: 12px; overflow: hidden; display: flex;",
|
||||
@@ -146,7 +147,7 @@ export const UI_STYLES = {
|
||||
columnBase: "display: flex; align-items: center; justify-content: center;",
|
||||
checkboxColumn: "flex: 0 0 44px; display: flex; align-items: center; justify-content: center; cursor: default;",
|
||||
contentColumn: "flex: 1; display: flex; flex-direction: column; transition: background-color 0.2s ease;",
|
||||
iconColumn: "width: 44px; flex-shrink: 0; font-size: 20px; color: #666;",
|
||||
iconColumn: "width: 44px; flex-shrink: 0; font-size: 20px; color: var(--dist-placeholder-add-color, #666);",
|
||||
|
||||
// Row and content styles
|
||||
infoRow: "display: flex; align-items: center; padding: 12px; cursor: pointer; min-height: 64px;",
|
||||
@@ -156,13 +157,15 @@ export const UI_STYLES = {
|
||||
buttonGroup: "display: flex; gap: 4px; margin-top: 10px;",
|
||||
settingsForm: "display: flex; flex-direction: column; gap: 10px;",
|
||||
checkboxGroup: "display: flex; align-items: center; gap: 8px; margin: 5px 0;",
|
||||
formLabelClickable: "font-size: 12px; color: #ccc; cursor: pointer;",
|
||||
formLabelClickable: "font-size: 12px; color: var(--dist-label-text, #ccc); cursor: pointer;",
|
||||
settingsToggle: "display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer; user-select: none;",
|
||||
controlsWrapper: "display: flex; gap: 6px; align-items: stretch; width: 100%;",
|
||||
|
||||
// Existing styles
|
||||
settingsArrow: "font-size: 12px; color: #888; transition: all 0.2s ease; margin-left: auto; padding: 4px;",
|
||||
infoBox: "color: #999; padding: 5px 14px; font-size: 11px; text-align: center; flex: 1; font-weight: 500;",
|
||||
settingsArrow:
|
||||
"font-size: 12px; color: var(--dist-settings-arrow, #888); transition: all 0.2s ease; margin-left: auto; padding: 4px;",
|
||||
infoBox:
|
||||
"color: var(--dist-info-box-text, #999); padding: 5px 14px; font-size: 11px; text-align: center; flex: 1; font-weight: 500;",
|
||||
workerSettings: "margin: 0 12px; padding: 0 12px;"
|
||||
};
|
||||
|
||||
|
||||
+140
-34
@@ -12,6 +12,90 @@
|
||||
--master-badge-fallback-border: #335533;
|
||||
--master-badge-delegate-bg: #3a3a3a;
|
||||
--master-badge-delegate-text: #ffcc66;
|
||||
--dist-divider: #444;
|
||||
--dist-muted-text: #888;
|
||||
--dist-label-text: #ccc;
|
||||
--dist-settings-arrow: #888;
|
||||
--dist-settings-arrow-hover: #fff;
|
||||
--dist-card-bg: #2a2a2a;
|
||||
--dist-card-title: #f4f5f7;
|
||||
--dist-card-subtext: #a9afb9;
|
||||
--dist-card-placeholder-title: #aaa;
|
||||
--dist-left-col-border: #3a3a3a;
|
||||
--dist-left-col-bg: rgba(0, 0, 0, 0.1);
|
||||
--dist-info-box-bg: #333;
|
||||
--dist-info-box-text: #999;
|
||||
--dist-input-bg: #2a2a2a;
|
||||
--dist-input-border: #444;
|
||||
--dist-input-text: #fff;
|
||||
--dist-settings-bg: #1e1e1e;
|
||||
--dist-settings-border: #2a2a2a;
|
||||
--dist-hover-bg: #333;
|
||||
--dist-placeholder-blueprint-border: #555;
|
||||
--dist-placeholder-blueprint-hover-border: #777;
|
||||
--dist-placeholder-add-border: #444;
|
||||
--dist-placeholder-add-hover-border: #666;
|
||||
--dist-placeholder-blueprint-bg: rgba(255, 255, 255, 0.02);
|
||||
--dist-placeholder-blueprint-hover-bg: rgba(255, 255, 255, 0.05);
|
||||
--dist-placeholder-add-hover-bg: rgba(255, 255, 255, 0.02);
|
||||
--dist-placeholder-blueprint-color: #777;
|
||||
--dist-placeholder-blueprint-hover-color: #999;
|
||||
--dist-placeholder-add-color: #555;
|
||||
--dist-placeholder-add-hover-color: #888;
|
||||
--dist-log-modal-bg: #1e1e1e;
|
||||
--dist-log-modal-border: #444;
|
||||
--dist-log-modal-header-border: #444;
|
||||
--dist-log-modal-title: #fff;
|
||||
--dist-log-modal-label: #ccc;
|
||||
--dist-log-modal-body-bg: #0d0d0d;
|
||||
--dist-log-modal-body-text: #ddd;
|
||||
--dist-log-modal-status: #888;
|
||||
}
|
||||
|
||||
.distributed-panel--light {
|
||||
--dist-divider: #c7ced9;
|
||||
--dist-muted-text: #5b6472;
|
||||
--dist-label-text: #2f3a4a;
|
||||
--dist-settings-arrow: #6b7483;
|
||||
--dist-settings-arrow-hover: #253040;
|
||||
--dist-card-bg: #f6f8fb;
|
||||
--dist-card-title: #1e293b;
|
||||
--dist-card-subtext: #4b5565;
|
||||
--dist-card-placeholder-title: #3f4a5a;
|
||||
--dist-left-col-border: #d8deea;
|
||||
--dist-left-col-bg: #eef2f8;
|
||||
--dist-info-box-bg: #e8edf5;
|
||||
--dist-info-box-text: #4f5b6c;
|
||||
--dist-input-bg: #ffffff;
|
||||
--dist-input-border: #b8c2d3;
|
||||
--dist-input-text: #1f2937;
|
||||
--dist-settings-bg: #eef2f8;
|
||||
--dist-settings-border: #d5dcea;
|
||||
--dist-hover-bg: #e6ebf3;
|
||||
--dist-placeholder-blueprint-border: #aeb8c9;
|
||||
--dist-placeholder-blueprint-hover-border: #8e9ab0;
|
||||
--dist-placeholder-add-border: #b7c1d3;
|
||||
--dist-placeholder-add-hover-border: #919eb4;
|
||||
--dist-placeholder-blueprint-bg: rgba(80, 100, 140, 0.05);
|
||||
--dist-placeholder-blueprint-hover-bg: rgba(80, 100, 140, 0.08);
|
||||
--dist-placeholder-add-hover-bg: rgba(80, 100, 140, 0.05);
|
||||
--dist-placeholder-blueprint-color: #5f6c82;
|
||||
--dist-placeholder-blueprint-hover-color: #47556e;
|
||||
--dist-placeholder-add-color: #6a7588;
|
||||
--dist-placeholder-add-hover-color: #4c586f;
|
||||
--master-badge-delegate-bg: #efe7ce;
|
||||
--master-badge-delegate-text: #6a4f00;
|
||||
--master-badge-fallback-bg: #dff1df;
|
||||
--master-badge-fallback-text: #1f5f1f;
|
||||
--master-badge-fallback-border: #9fc79f;
|
||||
--dist-log-modal-bg: #f9fbff;
|
||||
--dist-log-modal-border: #bcc7db;
|
||||
--dist-log-modal-header-border: #c7d1e3;
|
||||
--dist-log-modal-title: #1f2937;
|
||||
--dist-log-modal-label: #4a5568;
|
||||
--dist-log-modal-body-bg: #f2f5fb;
|
||||
--dist-log-modal-body-text: #1f2937;
|
||||
--dist-log-modal-status: #5c6779;
|
||||
}
|
||||
|
||||
.btn--stop {
|
||||
@@ -50,45 +134,47 @@
|
||||
}
|
||||
|
||||
.entity-card-content--hovered {
|
||||
background-color: #333 !important;
|
||||
background-color: var(--dist-hover-bg) !important;
|
||||
}
|
||||
|
||||
.placeholder-card--blueprint {
|
||||
border-color: #555 !important;
|
||||
background-color: rgba(255, 255, 255, 0.02) !important;
|
||||
border-color: var(--dist-placeholder-blueprint-border) !important;
|
||||
background-color: var(--dist-placeholder-blueprint-bg) !important;
|
||||
}
|
||||
|
||||
.placeholder-card--blueprint.is-hovered {
|
||||
border-color: #777 !important;
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: var(--dist-placeholder-blueprint-hover-border) !important;
|
||||
background-color: var(--dist-placeholder-blueprint-hover-bg) !important;
|
||||
}
|
||||
|
||||
.placeholder-card--add {
|
||||
border-color: #444 !important;
|
||||
border-color: var(--dist-placeholder-add-border) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.placeholder-card--add.is-hovered {
|
||||
border-color: #666 !important;
|
||||
background-color: rgba(255, 255, 255, 0.02) !important;
|
||||
border-color: var(--dist-placeholder-add-hover-border) !important;
|
||||
background-color: var(--dist-placeholder-add-hover-bg) !important;
|
||||
}
|
||||
|
||||
.placeholder-column--blueprint {
|
||||
color: #777 !important;
|
||||
color: var(--dist-placeholder-blueprint-color) !important;
|
||||
border-right-color: var(--dist-placeholder-blueprint-border) !important;
|
||||
}
|
||||
|
||||
.placeholder-column--blueprint.is-hovered {
|
||||
color: #999 !important;
|
||||
color: var(--dist-placeholder-blueprint-hover-color) !important;
|
||||
}
|
||||
|
||||
.placeholder-column--add {
|
||||
color: #555 !important;
|
||||
border-color: #444 !important;
|
||||
color: var(--dist-placeholder-add-color) !important;
|
||||
border-color: var(--dist-placeholder-add-border) !important;
|
||||
border-right-color: var(--dist-placeholder-add-border) !important;
|
||||
}
|
||||
|
||||
.placeholder-column--add.is-hovered {
|
||||
color: #888 !important;
|
||||
border-color: #666 !important;
|
||||
color: var(--dist-placeholder-add-hover-color) !important;
|
||||
border-color: var(--dist-placeholder-add-hover-border) !important;
|
||||
}
|
||||
|
||||
.tunnel-button--enable {
|
||||
@@ -110,42 +196,62 @@
|
||||
/* ---- Themeable card/column/input defaults (Classic) ---- */
|
||||
|
||||
.dist-card {
|
||||
background: #2a2a2a;
|
||||
background: var(--dist-card-bg);
|
||||
color: var(--dist-card-title);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dist-card--blueprint {
|
||||
border: 2px dashed #555;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 2px dashed var(--dist-placeholder-blueprint-border);
|
||||
background: var(--dist-placeholder-blueprint-bg);
|
||||
}
|
||||
|
||||
.dist-card--add {
|
||||
border: 1px dashed #444;
|
||||
border: 1px dashed var(--dist-placeholder-add-border);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dist-card__left-col {
|
||||
border-right: 1px solid #3a3a3a;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-right: 1px solid var(--dist-left-col-border);
|
||||
background: var(--dist-left-col-bg);
|
||||
}
|
||||
|
||||
.dist-info-box {
|
||||
background-color: #333;
|
||||
background-color: var(--dist-info-box-bg);
|
||||
color: var(--dist-info-box-text);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dist-form-input {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
background: var(--dist-input-bg);
|
||||
border: 1px solid var(--dist-input-border);
|
||||
color: var(--dist-input-text);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.worker-settings {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #2a2a2a;
|
||||
background: var(--dist-settings-bg);
|
||||
border: 1px solid var(--dist-settings-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dist-worker-info__title {
|
||||
color: var(--dist-card-title) !important;
|
||||
font-size: 1.03em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dist-worker-info__meta {
|
||||
color: var(--dist-card-subtext) !important;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.dist-worker-info__fallback {
|
||||
color: var(--master-badge-fallback-text) !important;
|
||||
font-size: 0.86em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Nodes 2.0 Theme ---- */
|
||||
|
||||
.distributed-panel--nodes2 .dist-card {
|
||||
@@ -235,19 +341,19 @@
|
||||
}
|
||||
|
||||
.log-modal__content {
|
||||
background: #1e1e1e;
|
||||
background: var(--dist-log-modal-bg);
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #444;
|
||||
border: 1px solid var(--dist-log-modal-border);
|
||||
}
|
||||
|
||||
.log-modal__header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #444;
|
||||
border-bottom: 1px solid var(--dist-log-modal-header-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -255,7 +361,7 @@
|
||||
|
||||
.log-modal__title {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
color: var(--dist-log-modal-title);
|
||||
}
|
||||
|
||||
.log-modal__header-buttons {
|
||||
@@ -276,7 +382,7 @@
|
||||
|
||||
.log-modal__refresh-label {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
color: var(--dist-log-modal-label);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -296,15 +402,15 @@
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #ddd;
|
||||
background: #0d0d0d;
|
||||
color: var(--dist-log-modal-body-text);
|
||||
background: var(--dist-log-modal-body-bg);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.log-modal__status {
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #444;
|
||||
border-top: 1px solid var(--dist-log-modal-header-border);
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: var(--dist-log-modal-status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { ENDPOINTS } from "./constants.js";
|
||||
|
||||
const NODE_CLASS = "DistributedValue";
|
||||
const WORKER_WIDGET_PREFIX = "_dv_worker_";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fetchWorkers() {
|
||||
try {
|
||||
const resp = await fetch(ENDPOINTS.CONFIG);
|
||||
if (!resp.ok) return [];
|
||||
const config = await resp.json();
|
||||
return config.workers || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hidden widget accessors (worker_values JSON storage)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getWorkerValuesWidget(node) {
|
||||
return node.widgets?.find((w) => w.name === "worker_values");
|
||||
}
|
||||
|
||||
function getHiddenWidget(node) {
|
||||
return node._dvHiddenWidget || getWorkerValuesWidget(node);
|
||||
}
|
||||
|
||||
function getWorkerWidgets(node) {
|
||||
return (node.widgets || []).filter((w) => w.name.startsWith(WORKER_WIDGET_PREFIX));
|
||||
}
|
||||
|
||||
function readStore(node) {
|
||||
const w = getHiddenWidget(node);
|
||||
if (!w) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(w.value || "{}");
|
||||
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(node, patch) {
|
||||
const w = getHiddenWidget(node);
|
||||
if (!w) return;
|
||||
const store = readStore(node);
|
||||
Object.assign(store, patch);
|
||||
w.value = JSON.stringify(store);
|
||||
}
|
||||
|
||||
function serializeWorkerValues(node) {
|
||||
const store = readStore(node);
|
||||
// Keep metadata keys (_type, _options) but overwrite worker values
|
||||
const newStore = { _type: store._type || "STRING" };
|
||||
if (store._options) newStore._options = store._options;
|
||||
for (const w of getWorkerWidgets(node)) {
|
||||
const key = w.name.slice(WORKER_WIDGET_PREFIX.length);
|
||||
const val = w.value;
|
||||
if (val !== undefined && val !== null && val !== "") {
|
||||
newStore[key] = String(val);
|
||||
}
|
||||
}
|
||||
const hidden = getHiddenWidget(node);
|
||||
if (hidden) hidden.value = JSON.stringify(newStore);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hide the raw worker_values widget
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function hideRawWidget(node) {
|
||||
const w = getWorkerValuesWidget(node);
|
||||
if (!w) return;
|
||||
|
||||
// Hide any DOM elements (multiline creates a textarea)
|
||||
if (w.element) w.element.style.display = "none";
|
||||
if (w.inputEl) w.inputEl.style.display = "none";
|
||||
|
||||
// Remove from visible widgets array
|
||||
const idx = node.widgets.indexOf(w);
|
||||
if (idx !== -1) node.widgets.splice(idx, 1);
|
||||
|
||||
// Keep reference
|
||||
node._dvHiddenWidget = w;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection type detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function detectTargetType(node) {
|
||||
// Find what our output (slot 0) is connected to
|
||||
if (!node.outputs || !node.outputs[0] || !node.outputs[0].links) {
|
||||
return { type: "STRING", options: null };
|
||||
}
|
||||
const linkIds = node.outputs[0].links;
|
||||
if (!linkIds || linkIds.length === 0) {
|
||||
return { type: "STRING", options: null };
|
||||
}
|
||||
|
||||
const graph = node.graph || app.graph;
|
||||
if (!graph) return { type: "STRING", options: null };
|
||||
|
||||
// Use the first connection to determine type
|
||||
const linkId = linkIds[0];
|
||||
const link = graph.links?.[linkId] || graph._links?.get?.(linkId);
|
||||
if (!link) return { type: "STRING", options: null };
|
||||
|
||||
const targetNode = graph.getNodeById(link.target_id);
|
||||
if (!targetNode) return { type: "STRING", options: null };
|
||||
|
||||
const targetInputName = targetNode.inputs?.[link.target_slot]?.name;
|
||||
if (!targetInputName) return { type: "STRING", options: null };
|
||||
|
||||
// Find the corresponding widget on the target node to get type info
|
||||
const targetWidget = targetNode.widgets?.find((w) => w.name === targetInputName);
|
||||
if (!targetWidget) {
|
||||
// Check node definition for input type
|
||||
const nodeDef = targetNode.constructor?.nodeData;
|
||||
if (nodeDef) {
|
||||
const inputDef =
|
||||
nodeDef.input?.required?.[targetInputName] ||
|
||||
nodeDef.input?.optional?.[targetInputName];
|
||||
if (inputDef) {
|
||||
if (Array.isArray(inputDef[0])) {
|
||||
return { type: "COMBO", options: inputDef[0] };
|
||||
}
|
||||
const typeName = inputDef[0];
|
||||
if (typeName === "INT") return { type: "INT", options: null };
|
||||
if (typeName === "FLOAT") return { type: "FLOAT", options: null };
|
||||
}
|
||||
}
|
||||
return { type: "STRING", options: null };
|
||||
}
|
||||
|
||||
// Detect type from widget
|
||||
if (targetWidget.type === "combo") {
|
||||
const options = targetWidget.options?.values || [];
|
||||
return { type: "COMBO", options };
|
||||
}
|
||||
if (targetWidget.type === "number") {
|
||||
// Distinguish INT vs FLOAT from the widget options
|
||||
const step = targetWidget.options?.step;
|
||||
if (step !== undefined && step % 1 === 0 && (!targetWidget.options?.precision || targetWidget.options.precision === 0)) {
|
||||
return { type: "INT", options: null };
|
||||
}
|
||||
return { type: "FLOAT", options: null };
|
||||
}
|
||||
|
||||
return { type: "STRING", options: null };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Worker widget creation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function removeWorkerWidgets(node) {
|
||||
for (let i = node.widgets.length - 1; i >= 0; i--) {
|
||||
if (node.widgets[i].name.startsWith(WORKER_WIDGET_PREFIX)) {
|
||||
node.widgets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createWorkerWidgets(node, workers, inputType, comboOptions) {
|
||||
removeWorkerWidgets(node);
|
||||
|
||||
const store = readStore(node);
|
||||
|
||||
for (let i = 0; i < workers.length; i++) {
|
||||
const worker = workers[i];
|
||||
const key = String(i + 1);
|
||||
const widgetName = `${WORKER_WIDGET_PREFIX}${key}`;
|
||||
const label = worker.name || worker.id || `Worker ${key}`;
|
||||
const savedValue = store[key] || "";
|
||||
|
||||
let widget;
|
||||
if (inputType === "COMBO" && Array.isArray(comboOptions) && comboOptions.length > 0) {
|
||||
widget = node.addWidget(
|
||||
"combo",
|
||||
widgetName,
|
||||
savedValue || comboOptions[0],
|
||||
(value) => {
|
||||
widget.value = value;
|
||||
serializeWorkerValues(node);
|
||||
},
|
||||
{ values: comboOptions }
|
||||
);
|
||||
} else if (inputType === "INT") {
|
||||
widget = node.addWidget(
|
||||
"number",
|
||||
widgetName,
|
||||
savedValue ? Number(savedValue) : 0,
|
||||
(value) => {
|
||||
widget.value = value;
|
||||
serializeWorkerValues(node);
|
||||
},
|
||||
{ min: -Infinity, max: Infinity, step: 1, precision: 0 }
|
||||
);
|
||||
} else if (inputType === "FLOAT") {
|
||||
widget = node.addWidget(
|
||||
"number",
|
||||
widgetName,
|
||||
savedValue ? Number(savedValue) : 0.0,
|
||||
(value) => {
|
||||
widget.value = value;
|
||||
serializeWorkerValues(node);
|
||||
},
|
||||
{ min: -Infinity, max: Infinity, step: 0.1, precision: 3 }
|
||||
);
|
||||
} else {
|
||||
// STRING (default)
|
||||
widget = node.addWidget(
|
||||
"string",
|
||||
widgetName,
|
||||
savedValue,
|
||||
(value) => {
|
||||
widget.value = value;
|
||||
serializeWorkerValues(node);
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
widget.label = label;
|
||||
}
|
||||
|
||||
// Persist type metadata
|
||||
const patch = { _type: inputType };
|
||||
if (comboOptions) patch._options = comboOptions;
|
||||
writeStore(node, patch);
|
||||
|
||||
// Re-serialize worker values after creating widgets
|
||||
serializeWorkerValues(node);
|
||||
|
||||
// Resize node to fit
|
||||
const size = node.computeSize();
|
||||
size[0] = Math.max(size[0], node.size?.[0] || 0);
|
||||
node.setSize(size);
|
||||
if (node.setDirtyCanvas) node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rebuild widgets based on current connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function rebuildWidgets(node, workers) {
|
||||
const { type, options } = detectTargetType(node);
|
||||
createWorkerWidgets(node, workers, type, options);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.registerExtension({
|
||||
name: "Distributed.DistributedValue",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== NODE_CLASS) return;
|
||||
|
||||
try {
|
||||
const workers = await fetchWorkers();
|
||||
node._dvWorkers = workers;
|
||||
|
||||
// Hide the raw worker_values widget
|
||||
hideRawWidget(node);
|
||||
|
||||
// Read stored type or detect from connection
|
||||
const store = readStore(node);
|
||||
const storedType = store._type || "STRING";
|
||||
const storedOptions = store._options || null;
|
||||
|
||||
if (workers.length > 0) {
|
||||
createWorkerWidgets(node, workers, storedType, storedOptions);
|
||||
}
|
||||
|
||||
// Listen for connection changes to adapt widget type
|
||||
const originalOnConnectionsChange = node.onConnectionsChange;
|
||||
node.onConnectionsChange = function (side, slotIndex, connected, linkInfo, ioSlot) {
|
||||
if (originalOnConnectionsChange) {
|
||||
originalOnConnectionsChange.call(this, side, slotIndex, connected, linkInfo, ioSlot);
|
||||
}
|
||||
// side 2 = output; slot 0 = our "value" output
|
||||
if (slotIndex === 0 && this._dvWorkers?.length > 0) {
|
||||
// Small delay to let litegraph finish link setup
|
||||
setTimeout(() => rebuildWidgets(this, this._dvWorkers), 50);
|
||||
}
|
||||
};
|
||||
|
||||
// Override configure for workflow load
|
||||
const originalConfigure = node.configure;
|
||||
node.configure = function (data) {
|
||||
const result = originalConfigure
|
||||
? originalConfigure.call(this, data)
|
||||
: undefined;
|
||||
|
||||
setTimeout(() => {
|
||||
hideRawWidget(this);
|
||||
if (this._dvWorkers?.length > 0) {
|
||||
const s = readStore(this);
|
||||
createWorkerWidgets(this, this._dvWorkers, s._type || "STRING", s._options || null);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return result;
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in DistributedValue extension:", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
+102
@@ -37,6 +37,7 @@ class DistributedExtension {
|
||||
|
||||
// Initialize abort controller for status checks
|
||||
this.statusCheckAbortController = null;
|
||||
this.themeMutationObserver = null;
|
||||
|
||||
// Inject CSS for pulsing animation
|
||||
this.injectStyles();
|
||||
@@ -264,6 +265,7 @@ class DistributedExtension {
|
||||
this.onPanelOpen();
|
||||
renderSidebarContent(this, el);
|
||||
this._applyNodes2Style();
|
||||
this._applyThemeToneClass();
|
||||
},
|
||||
destroy: () => {
|
||||
this.onPanelClose();
|
||||
@@ -276,6 +278,8 @@ class DistributedExtension {
|
||||
if (!this.statusCheckTimeout) {
|
||||
checkAllWorkerStatuses(this);
|
||||
}
|
||||
this._startThemeObserver();
|
||||
this._applyThemeToneClass();
|
||||
}
|
||||
|
||||
onPanelClose() {
|
||||
@@ -292,6 +296,7 @@ class DistributedExtension {
|
||||
clearTimeout(this.statusCheckTimeout);
|
||||
this.statusCheckTimeout = null;
|
||||
}
|
||||
this._stopThemeObserver();
|
||||
|
||||
this.panelElement = null;
|
||||
}
|
||||
@@ -302,11 +307,108 @@ class DistributedExtension {
|
||||
this.panelElement.classList.toggle('distributed-panel--nodes2', Boolean(enabled));
|
||||
}
|
||||
|
||||
_parseColorToRgba(colorValue) {
|
||||
if (!colorValue || typeof colorValue !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = colorValue.trim().toLowerCase();
|
||||
if (!color || color === "transparent") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rgbMatch = color.match(/^rgba?\(([^)]+)\)$/);
|
||||
if (rgbMatch) {
|
||||
const parts = rgbMatch[1].split(",").map((part) => Number(part.trim()));
|
||||
if (parts.length < 3 || parts.slice(0, 3).some((part) => Number.isNaN(part))) {
|
||||
return null;
|
||||
}
|
||||
const alpha = parts.length >= 4 && Number.isFinite(parts[3]) ? parts[3] : 1;
|
||||
return {
|
||||
r: Math.max(0, Math.min(255, parts[0])),
|
||||
g: Math.max(0, Math.min(255, parts[1])),
|
||||
b: Math.max(0, Math.min(255, parts[2])),
|
||||
a: Math.max(0, Math.min(1, alpha)),
|
||||
};
|
||||
}
|
||||
|
||||
const hexMatch = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
||||
if (hexMatch) {
|
||||
const value = hexMatch[1];
|
||||
const expanded = value.length === 3
|
||||
? value.split("").map((c) => `${c}${c}`).join("")
|
||||
: value;
|
||||
const r = parseInt(expanded.slice(0, 2), 16);
|
||||
const g = parseInt(expanded.slice(2, 4), 16);
|
||||
const b = parseInt(expanded.slice(4, 6), 16);
|
||||
return { r, g, b, a: 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_isPanelLightTheme() {
|
||||
const fallbackLight = window.matchMedia?.("(prefers-color-scheme: light)")?.matches || false;
|
||||
if (!this.panelElement) {
|
||||
return fallbackLight;
|
||||
}
|
||||
|
||||
let current = this.panelElement;
|
||||
while (current) {
|
||||
const bg = getComputedStyle(current).backgroundColor;
|
||||
const rgba = this._parseColorToRgba(bg);
|
||||
if (rgba && rgba.a > 0.02) {
|
||||
// Relative luminance approximation (0..1)
|
||||
const luminance = (0.2126 * rgba.r + 0.7152 * rgba.g + 0.0722 * rgba.b) / 255;
|
||||
return luminance > 0.58;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return fallbackLight;
|
||||
}
|
||||
|
||||
_applyThemeToneClass() {
|
||||
if (!this.panelElement) {
|
||||
return;
|
||||
}
|
||||
this.panelElement.classList.toggle("distributed-panel--light", this._isPanelLightTheme());
|
||||
}
|
||||
|
||||
_startThemeObserver() {
|
||||
if (this.themeMutationObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.themeMutationObserver = new MutationObserver(() => {
|
||||
this._applyThemeToneClass();
|
||||
});
|
||||
this.themeMutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style"],
|
||||
});
|
||||
if (document.body) {
|
||||
this.themeMutationObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopThemeObserver() {
|
||||
if (!this.themeMutationObserver) {
|
||||
return;
|
||||
}
|
||||
this.themeMutationObserver.disconnect();
|
||||
this.themeMutationObserver = null;
|
||||
}
|
||||
|
||||
_setupNodes2Listener() {
|
||||
app.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", (e) => {
|
||||
const enabled = e.detail?.value ?? false;
|
||||
if (this.panelElement) {
|
||||
this.panelElement.classList.toggle('distributed-panel--nodes2', Boolean(enabled));
|
||||
this._applyThemeToneClass();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { BUTTON_STYLES } from "../constants.js";
|
||||
|
||||
export function renderActionsSection(extension) {
|
||||
const actionsSection = document.createElement("div");
|
||||
actionsSection.style.cssText = "padding-top: 10px; margin-bottom: 15px; border-top: 1px solid #444;";
|
||||
actionsSection.style.cssText =
|
||||
"padding-top: 10px; margin-bottom: 15px; border-top: 1px solid var(--dist-divider, #444);";
|
||||
|
||||
const buttonRow = document.createElement("div");
|
||||
buttonRow.style.cssText = "display: flex; gap: 8px;";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createCheckboxSetting, createNumberSetting } from "../ui/buttonHelpers.
|
||||
|
||||
export function renderSettingsSection(extension) {
|
||||
const settingsSection = document.createElement("div");
|
||||
settingsSection.style.cssText = "border-top: 1px solid #444; margin-bottom: 10px;";
|
||||
settingsSection.style.cssText = "border-top: 1px solid var(--dist-divider, #444); margin-bottom: 10px;";
|
||||
|
||||
const settingsToggleArea = document.createElement("div");
|
||||
settingsToggleArea.style.cssText = "padding: 16.5px 0; cursor: pointer; user-select: none;";
|
||||
@@ -16,21 +16,22 @@ export function renderSettingsSection(extension) {
|
||||
|
||||
const workerSettingsToggle = document.createElement("span");
|
||||
workerSettingsToggle.textContent = "▶";
|
||||
workerSettingsToggle.style.cssText = "font-size: 12px; color: #888; transition: all 0.2s ease;";
|
||||
workerSettingsToggle.style.cssText =
|
||||
"font-size: 12px; color: var(--dist-settings-arrow, #888); transition: all 0.2s ease;";
|
||||
|
||||
settingsHeader.appendChild(workerSettingsTitle);
|
||||
settingsHeader.appendChild(workerSettingsToggle);
|
||||
settingsToggleArea.appendChild(settingsHeader);
|
||||
|
||||
settingsToggleArea.onmouseover = () => {
|
||||
workerSettingsToggle.style.color = "#fff";
|
||||
workerSettingsToggle.style.color = "var(--dist-settings-arrow-hover, #fff)";
|
||||
};
|
||||
settingsToggleArea.onmouseout = () => {
|
||||
workerSettingsToggle.style.color = "#888";
|
||||
workerSettingsToggle.style.color = "var(--dist-settings-arrow, #888)";
|
||||
};
|
||||
|
||||
const settingsSeparator = document.createElement("div");
|
||||
settingsSeparator.style.cssText = "border-bottom: 1px solid #444; margin: 0;";
|
||||
settingsSeparator.style.cssText = "border-bottom: 1px solid var(--dist-divider, #444); margin: 0;";
|
||||
|
||||
const settingsContent = document.createElement("div");
|
||||
settingsContent.style.cssText =
|
||||
@@ -59,12 +60,12 @@ export function renderSettingsSection(extension) {
|
||||
const generalLabel = document.createElement("div");
|
||||
generalLabel.textContent = "GENERAL";
|
||||
generalLabel.style.cssText =
|
||||
"grid-column: 1 / span 2; font-size: 11px; color: #888; letter-spacing: 0.06em; padding-top: 2px;";
|
||||
"grid-column: 1 / span 2; font-size: 11px; color: var(--dist-muted-text, #888); letter-spacing: 0.06em; padding-top: 2px;";
|
||||
|
||||
const timeoutsLabel = document.createElement("div");
|
||||
timeoutsLabel.textContent = "TIMEOUTS";
|
||||
timeoutsLabel.style.cssText =
|
||||
"grid-column: 1 / span 2; font-size: 11px; color: #888; letter-spacing: 0.06em; padding-top: 4px;";
|
||||
"grid-column: 1 / span 2; font-size: 11px; color: var(--dist-muted-text, #888); letter-spacing: 0.06em; padding-top: 4px;";
|
||||
|
||||
settingsDiv.appendChild(generalLabel);
|
||||
settingsDiv.appendChild(
|
||||
|
||||
@@ -52,8 +52,9 @@ export async function renderSidebarContent(extension, el) {
|
||||
// Show loading indicator
|
||||
el.innerHTML = '';
|
||||
const loadingDiv = document.createElement("div");
|
||||
loadingDiv.style.cssText = "display: flex; align-items: center; justify-content: center; height: calc(100vh - 100px); color: #888;";
|
||||
loadingDiv.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" style="color: #888;">
|
||||
loadingDiv.style.cssText =
|
||||
"display: flex; align-items: center; justify-content: center; height: calc(100vh - 100px); color: var(--dist-muted-text, #888);";
|
||||
loadingDiv.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" style="color: var(--dist-muted-text, #888);">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="40 40"/>
|
||||
</svg>`;
|
||||
el.appendChild(loadingDiv);
|
||||
@@ -74,7 +75,8 @@ export async function renderSidebarContent(extension, el) {
|
||||
// Create toolbar header to match ComfyUI style
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "p-toolbar p-component border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8";
|
||||
toolbar.style.cssText = "border-bottom: 1px solid #444; background: transparent; display: flex; align-items: center;";
|
||||
toolbar.style.cssText =
|
||||
"border-bottom: 1px solid var(--dist-divider, #444); background: transparent; display: flex; align-items: center;";
|
||||
const toolbarStart = document.createElement("div");
|
||||
toolbarStart.className = "p-toolbar-start";
|
||||
toolbarStart.style.cssText = "display: flex; align-items: center;";
|
||||
@@ -107,6 +109,7 @@ export async function renderSidebarContent(extension, el) {
|
||||
container.appendChild(renderActionsSection(extension));
|
||||
container.appendChild(renderSettingsSection(extension));
|
||||
el.appendChild(container);
|
||||
extension._applyThemeToneClass?.();
|
||||
// Start checking worker statuses immediately in parallel
|
||||
setTimeout(() => checkAllWorkerStatuses(extension), 0);
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { addNewWorker, isRemoteWorker } from "../workerSettings.js";
|
||||
|
||||
|
||||
describe("workerSettings remote classification", () => {
|
||||
let originalWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
originalWindow = globalThis.window;
|
||||
globalThis.window = {
|
||||
location: {
|
||||
hostname: "127.0.0.1",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.window = originalWindow;
|
||||
});
|
||||
|
||||
it("treats explicit local worker type as local even with non-local host", () => {
|
||||
const worker = { type: "local", host: "192.168.1.50" };
|
||||
expect(isRemoteWorker({}, worker)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats explicit remote worker type as remote", () => {
|
||||
const worker = { type: "remote", host: "127.0.0.1" };
|
||||
expect(isRemoteWorker({}, worker)).toBe(true);
|
||||
});
|
||||
|
||||
it("treats cloud worker type as remote", () => {
|
||||
const worker = { type: "cloud", host: "worker.example.com" };
|
||||
expect(isRemoteWorker({}, worker)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to host heuristic for legacy workers", () => {
|
||||
expect(isRemoteWorker({}, { host: "127.0.0.1" })).toBe(false);
|
||||
expect(isRemoteWorker({}, { host: "worker.example.com" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addNewWorker GPU availability guard", () => {
|
||||
it("falls back to a disabled remote worker when no local CUDA device is available", async () => {
|
||||
const toastAdd = vi.fn();
|
||||
const updateWorker = vi.fn().mockResolvedValue({});
|
||||
const stateUpdateWorker = vi.fn();
|
||||
const extension = {
|
||||
cudaDeviceCount: 1,
|
||||
masterCudaDevice: 0,
|
||||
panelElement: null,
|
||||
config: {
|
||||
workers: [],
|
||||
master: { cuda_device: 0 },
|
||||
},
|
||||
api: { updateWorker },
|
||||
state: { updateWorker: stateUpdateWorker, setWorkerExpanded: vi.fn() },
|
||||
app: { extensionManager: { toast: { add: toastAdd } } },
|
||||
};
|
||||
|
||||
await addNewWorker(extension);
|
||||
|
||||
expect(updateWorker).toHaveBeenCalledTimes(1);
|
||||
expect(updateWorker.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "remote",
|
||||
enabled: false,
|
||||
cuda_device: null,
|
||||
host: "",
|
||||
})
|
||||
);
|
||||
expect(extension.config.workers).toHaveLength(1);
|
||||
expect(extension.config.workers[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
type: "remote",
|
||||
enabled: false,
|
||||
cuda_device: null,
|
||||
host: "",
|
||||
})
|
||||
);
|
||||
expect(stateUpdateWorker).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ enabled: false })
|
||||
);
|
||||
expect(toastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: "warn",
|
||||
summary: "Remote Worker Added",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("assigns the first free local CUDA device when adding a worker", async () => {
|
||||
const toastAdd = vi.fn();
|
||||
const updateWorker = vi.fn().mockResolvedValue({});
|
||||
const stateUpdateWorker = vi.fn();
|
||||
const setWorkerExpanded = vi.fn();
|
||||
const extension = {
|
||||
cudaDeviceCount: 3,
|
||||
masterCudaDevice: 0,
|
||||
panelElement: null,
|
||||
config: {
|
||||
workers: [
|
||||
{ id: "w-existing", type: "local", port: 8189, cuda_device: 1, enabled: true },
|
||||
],
|
||||
master: { cuda_device: 0 },
|
||||
},
|
||||
api: { updateWorker },
|
||||
state: { updateWorker: stateUpdateWorker, setWorkerExpanded },
|
||||
app: { extensionManager: { toast: { add: toastAdd } } },
|
||||
};
|
||||
|
||||
await addNewWorker(extension);
|
||||
|
||||
expect(updateWorker).toHaveBeenCalledTimes(1);
|
||||
expect(updateWorker.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
cuda_device: 2,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -27,12 +27,10 @@ const cardConfigs = {
|
||||
const participationEnabled = extension.isMasterParticipationEnabled();
|
||||
const fallbackActive = extension.isMasterFallbackActive();
|
||||
let delegateBadge = '';
|
||||
if (!participationEnabled) {
|
||||
delegateBadge = fallbackActive
|
||||
? `<br><small style="color: #6bd06b;">Fallback active • Master executing</small>`
|
||||
: `<br><small style="color: ${UI_COLORS.SECONDARY_TEXT};">Orchestrator-only mode</small>`;
|
||||
if (!participationEnabled && fallbackActive) {
|
||||
delegateBadge = `<br><span class="dist-worker-info__fallback">Fallback active • Master executing</span>`;
|
||||
}
|
||||
return `<strong id="master-name-display">${data?.name || extension.config?.master?.name || "Master"}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};"><span id="master-cuda-info">${cudaInfo}Port ${port}</span></small>${delegateBadge}`;
|
||||
return `<span class="dist-worker-info__title" id="master-name-display">${data?.name || extension.config?.master?.name || "Master"}</span><br><span class="dist-worker-info__meta"><span id="master-cuda-info">${cudaInfo}Port ${port}</span></span>${delegateBadge}`;
|
||||
},
|
||||
controls: {
|
||||
type: 'master'
|
||||
@@ -63,12 +61,15 @@ const cardConfigs = {
|
||||
|
||||
if (isCloud) {
|
||||
// For cloud workers, don't show port (it's always 443)
|
||||
return `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${data.host}</small>`;
|
||||
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${data.host}</span>`;
|
||||
} else if (isRemote) {
|
||||
return `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${data.host}:${data.port}</small>`;
|
||||
const hostLabel = data.host
|
||||
? `${data.host}:${data.port}`
|
||||
: `Unconfigured remote worker • Port ${data.port}`;
|
||||
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${hostLabel}</span>`;
|
||||
} else {
|
||||
const cudaInfo = data.cuda_device !== undefined ? `CUDA ${data.cuda_device} • ` : '';
|
||||
return `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${cudaInfo}Port ${data.port}</small>`;
|
||||
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${cudaInfo}Port ${data.port}</span>`;
|
||||
}
|
||||
},
|
||||
controls: {
|
||||
@@ -401,10 +402,15 @@ export class DistributedUI {
|
||||
|
||||
const modal = createLogModal();
|
||||
this._logModal = modal;
|
||||
const themeClass =
|
||||
extension.panelElement?.classList.contains("distributed-panel--light")
|
||||
? "distributed-panel--light"
|
||||
: "";
|
||||
modal.mount(document.body, {
|
||||
workerName,
|
||||
logData,
|
||||
fetchLog: fetchLog || (async () => extension.api.getWorkerLog(workerId, 1000)),
|
||||
themeClass,
|
||||
onClose: () => {
|
||||
if (this._logModal === modal) {
|
||||
this._logModal = null;
|
||||
@@ -427,7 +433,8 @@ export class DistributedUI {
|
||||
|
||||
const settingsToggle = document.createElement("span");
|
||||
settingsToggle.textContent = "▶"; // Right arrow when collapsed
|
||||
settingsToggle.style.cssText = "font-size: 12px; color: #888; transition: all 0.2s ease;";
|
||||
settingsToggle.style.cssText =
|
||||
"font-size: 12px; color: var(--dist-settings-arrow, #888); transition: all 0.2s ease;";
|
||||
|
||||
settingsRow.appendChild(settingsToggle);
|
||||
settingsRow.appendChild(settingsTitle);
|
||||
@@ -574,16 +581,22 @@ export class DistributedUI {
|
||||
badge.textContent = message;
|
||||
badge.classList.add("master-info-badge--fallback");
|
||||
} else if (!participationEnabled) {
|
||||
message = "Master disabled: running as orchestrator only.";
|
||||
message = "Master disabled: running as orchestrator only";
|
||||
badge.textContent = message;
|
||||
badge.classList.add("master-info-badge--delegate");
|
||||
} else {
|
||||
message = "Master participating in workflows.";
|
||||
message = "Master participating in workflows";
|
||||
badge.textContent = message;
|
||||
}
|
||||
controlsWrapper.appendChild(badge);
|
||||
} else if (config.dynamic && data) {
|
||||
if (isRemote) {
|
||||
const isCloud = data.type === 'cloud';
|
||||
const workerTypeText = isCloud ? "Cloud worker" : "Remote worker";
|
||||
const workerTypeBadge = this.createInfoBox(workerTypeText);
|
||||
workerTypeBadge.title = "Worker is externally hosted";
|
||||
controlsWrapper.appendChild(workerTypeBadge);
|
||||
|
||||
const logBtn = this.createButton('View Log', () => viewWorkerLog(extension, data.id, true));
|
||||
logBtn.id = `log-${data.id}`;
|
||||
logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
|
||||
|
||||
@@ -15,7 +15,7 @@ export function createCheckboxSetting(id, label, tooltip, checked, onChange) {
|
||||
const lbl = document.createElement("label");
|
||||
lbl.htmlFor = id;
|
||||
lbl.textContent = label;
|
||||
lbl.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;";
|
||||
lbl.style.cssText = "font-size: 12px; color: var(--dist-label-text, #ccc); cursor: pointer;";
|
||||
if (tooltip) {
|
||||
lbl.title = tooltip;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export function createNumberSetting(id, label, tooltip, value, min, step, onChan
|
||||
const lbl = document.createElement("label");
|
||||
lbl.htmlFor = id;
|
||||
lbl.textContent = label;
|
||||
lbl.style.cssText = "font-size: 12px; color: #ccc;";
|
||||
lbl.style.cssText = "font-size: 12px; color: var(--dist-label-text, #ccc);";
|
||||
if (tooltip) {
|
||||
lbl.title = tooltip;
|
||||
}
|
||||
@@ -42,7 +42,8 @@ export function createNumberSetting(id, label, tooltip, value, min, step, onChan
|
||||
input.id = id;
|
||||
input.min = String(min);
|
||||
input.step = String(step);
|
||||
input.style.cssText = "width: 80px; padding: 2px 6px; background: #222; color: #ddd; border: 1px solid #333; border-radius: 3px;";
|
||||
input.style.cssText =
|
||||
"width: 80px; padding: 2px 6px; background: var(--dist-input-bg, #222); color: var(--dist-input-text, #ddd); border: 1px solid var(--dist-input-border, #333); border-radius: 3px;";
|
||||
input.value = value;
|
||||
input.onchange = onChange;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export function renderEntityCard(ui, cardConfigs, entityType, data, extension) {
|
||||
workerContent.appendChild(statusDot);
|
||||
|
||||
const infoSpan = document.createElement("span");
|
||||
infoSpan.classList.add("dist-worker-info");
|
||||
infoSpan.innerHTML = config.infoText(data, extension);
|
||||
workerContent.appendChild(infoSpan);
|
||||
|
||||
@@ -92,13 +93,13 @@ export function renderEntityCard(ui, cardConfigs, entityType, data, extension) {
|
||||
rightColumn.onmouseover = () => {
|
||||
rightColumn.classList.add("entity-card-content--hovered");
|
||||
if (settingsArrow) {
|
||||
settingsArrow.style.color = "#fff";
|
||||
settingsArrow.style.color = "var(--dist-settings-arrow-hover, #fff)";
|
||||
}
|
||||
};
|
||||
rightColumn.onmouseout = () => {
|
||||
rightColumn.classList.remove("entity-card-content--hovered");
|
||||
if (settingsArrow) {
|
||||
settingsArrow.style.color = "#888";
|
||||
settingsArrow.style.color = "var(--dist-settings-arrow, #888)";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+4
-1
@@ -95,13 +95,16 @@ export function createLogModal() {
|
||||
}
|
||||
};
|
||||
|
||||
const mount = (container, { workerName, logData, onClose, fetchLog }) => {
|
||||
const mount = (container, { workerName, logData, onClose, fetchLog, themeClass = "" }) => {
|
||||
_onClose = onClose;
|
||||
_fetchLog = fetchLog;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'distributed-log-modal';
|
||||
modal.className = 'log-modal';
|
||||
if (themeClass) {
|
||||
modal.classList.add(themeClass);
|
||||
}
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'log-modal__content';
|
||||
|
||||
@@ -14,11 +14,12 @@ export function createWorkerSettingsForm(ui, extension, worker) {
|
||||
const typeLabel = document.createElement("label");
|
||||
typeLabel.htmlFor = `worker-type-${worker.id}`;
|
||||
typeLabel.textContent = "Worker Type:";
|
||||
typeLabel.style.cssText = "font-size: 12px; color: #ccc;";
|
||||
typeLabel.style.cssText = "font-size: 12px; color: var(--dist-label-text, #ccc);";
|
||||
|
||||
const typeSelect = document.createElement("select");
|
||||
typeSelect.id = `worker-type-${worker.id}`;
|
||||
typeSelect.style.cssText = "padding: 4px 8px; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 12px;";
|
||||
typeSelect.style.cssText =
|
||||
"padding: 4px 8px; background: var(--dist-input-bg, #333); color: var(--dist-input-text, #fff); border: 1px solid var(--dist-input-border, #555); border-radius: 4px; font-size: 12px;";
|
||||
|
||||
const localOption = document.createElement("option");
|
||||
localOption.value = "local";
|
||||
|
||||
+81
-18
@@ -4,14 +4,22 @@ import { parseHostInput } from './urlUtils.js';
|
||||
import { toggleWorkerExpanded } from './workerLifecycle.js';
|
||||
|
||||
export function isRemoteWorker(extension, worker) {
|
||||
// Check if explicitly marked as cloud worker
|
||||
if (worker.type === "cloud") {
|
||||
const workerType = String(worker?.type || "").toLowerCase();
|
||||
|
||||
// Explicit type always wins over host heuristics.
|
||||
if (workerType === "cloud" || workerType === "remote") {
|
||||
return true;
|
||||
}
|
||||
if (workerType === "local") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise check by host (backward compatibility)
|
||||
const parsed = parseHostInput(worker.host || window.location.hostname);
|
||||
const host = parsed.host || window.location.hostname;
|
||||
return host !== "localhost" && host !== "127.0.0.1" && host !== window.location.hostname;
|
||||
const parsed = parseHostInput(worker?.host || window.location.hostname);
|
||||
const host = String(parsed.host || window.location.hostname || "").toLowerCase();
|
||||
const currentHost = String(parseHostInput(window.location.hostname).host || window.location.hostname || "").toLowerCase();
|
||||
const localHosts = new Set(["", "localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
||||
return !(localHosts.has(host) || host === currentHost);
|
||||
}
|
||||
|
||||
export function isCloudWorker(extension, worker) {
|
||||
@@ -32,6 +40,7 @@ export async function saveWorkerSettings(extension, workerId) {
|
||||
const rawHost = isRemote ? document.getElementById(`host-${workerId}`).value : window.location.hostname;
|
||||
const parsedHost = isRemote ? parseHostInput(rawHost) : { host: window.location.hostname, port: null };
|
||||
const host = isRemote ? parsedHost.host : window.location.hostname;
|
||||
const hostTrimmed = (host || "").trim();
|
||||
let port = parseInt(document.getElementById(`port-${workerId}`).value);
|
||||
const cudaDevice = isRemote ? undefined : parseInt(document.getElementById(`cuda-${workerId}`).value);
|
||||
const extraArgs = isRemote ? undefined : document.getElementById(`args-${workerId}`).value;
|
||||
@@ -51,7 +60,7 @@ export async function saveWorkerSettings(extension, workerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((workerType === "remote" || workerType === "cloud") && !host.trim()) {
|
||||
if ((workerType === "remote" || workerType === "cloud") && !hostTrimmed) {
|
||||
extension.app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "Validation Error",
|
||||
@@ -102,7 +111,7 @@ export async function saveWorkerSettings(extension, workerId) {
|
||||
}
|
||||
} else {
|
||||
// For remote workers, only check conflicts with other workers on the same host
|
||||
const sameHostConflict = extension.config.workers.some((w) => w.id !== workerId && w.port === port && w.host === host.trim());
|
||||
const sameHostConflict = extension.config.workers.some((w) => w.id !== workerId && w.port === port && w.host === hostTrimmed);
|
||||
|
||||
if (sameHostConflict) {
|
||||
extension.app.extensionManager.toast.add({
|
||||
@@ -115,21 +124,28 @@ export async function saveWorkerSettings(extension, workerId) {
|
||||
}
|
||||
}
|
||||
|
||||
const wasUnconfiguredRemote =
|
||||
(worker.type === "remote" || worker.type === "cloud") &&
|
||||
(!String(worker.host || "").trim()) &&
|
||||
!worker.enabled;
|
||||
const nextEnabled = isRemote && hostTrimmed && wasUnconfiguredRemote ? true : worker.enabled;
|
||||
|
||||
try {
|
||||
await extension.api.updateWorker(workerId, {
|
||||
name: name.trim(),
|
||||
type: workerType,
|
||||
host: isRemote ? host.trim() : null,
|
||||
host: isRemote ? hostTrimmed : null,
|
||||
port,
|
||||
cuda_device: isRemote ? null : cudaDevice,
|
||||
extra_args: isRemote ? null : extraArgs ? extraArgs.trim() : "",
|
||||
enabled: nextEnabled,
|
||||
});
|
||||
|
||||
// Update local config
|
||||
worker.name = name.trim();
|
||||
worker.type = workerType;
|
||||
if (isRemote) {
|
||||
worker.host = host.trim();
|
||||
worker.host = hostTrimmed;
|
||||
delete worker.cuda_device;
|
||||
delete worker.extra_args;
|
||||
} else {
|
||||
@@ -138,14 +154,17 @@ export async function saveWorkerSettings(extension, workerId) {
|
||||
worker.extra_args = extraArgs ? extraArgs.trim() : "";
|
||||
}
|
||||
worker.port = port;
|
||||
worker.enabled = nextEnabled;
|
||||
|
||||
// Sync to state
|
||||
extension.state.updateWorker(workerId, { enabled: worker.enabled });
|
||||
extension.state.updateWorker(workerId, { enabled: nextEnabled });
|
||||
|
||||
extension.app.extensionManager.toast.add({
|
||||
severity: "success",
|
||||
summary: "Settings Saved",
|
||||
detail: `Worker ${name} settings updated`,
|
||||
detail: nextEnabled && wasUnconfiguredRemote
|
||||
? `Worker ${name} configured and enabled`
|
||||
: `Worker ${name} settings updated`,
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
@@ -229,6 +248,44 @@ export async function deleteWorker(extension, workerId) {
|
||||
}
|
||||
|
||||
export async function addNewWorker(extension) {
|
||||
const toInt = (value) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const totalCudaDevices = toInt(extension.cudaDeviceCount);
|
||||
const masterCudaDevice = toInt(extension.masterCudaDevice ?? extension.config?.master?.cuda_device);
|
||||
const localWorkers = (extension.config?.workers || []).filter((w) => !isRemoteWorker(extension, w));
|
||||
|
||||
let selectedCudaDevice = extension.config.workers.length;
|
||||
let fallbackToRemote = false;
|
||||
if (totalCudaDevices !== null && totalCudaDevices > 0) {
|
||||
const usedCudaDevices = new Set();
|
||||
for (const worker of localWorkers) {
|
||||
const cudaIdx = toInt(worker.cuda_device);
|
||||
if (cudaIdx !== null) {
|
||||
usedCudaDevices.add(cudaIdx);
|
||||
}
|
||||
}
|
||||
if (masterCudaDevice !== null) {
|
||||
usedCudaDevices.add(masterCudaDevice);
|
||||
}
|
||||
|
||||
const availableCudaDevices = [];
|
||||
for (let i = 0; i < totalCudaDevices; i++) {
|
||||
if (!usedCudaDevices.has(i)) {
|
||||
availableCudaDevices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (availableCudaDevices.length === 0) {
|
||||
fallbackToRemote = true;
|
||||
selectedCudaDevice = null;
|
||||
} else {
|
||||
selectedCudaDevice = availableCudaDevices[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new worker ID using UUID (fallback for non-secure contexts)
|
||||
const newId = generateUUID();
|
||||
|
||||
@@ -244,8 +301,10 @@ export async function addNewWorker(extension) {
|
||||
id: newId,
|
||||
name: `Worker ${extension.config.workers.length + 1}`,
|
||||
port: nextPort,
|
||||
cuda_device: extension.config.workers.length,
|
||||
enabled: true, // Default to enabled for convenience
|
||||
type: fallbackToRemote ? "remote" : "local",
|
||||
host: fallbackToRemote ? "" : null,
|
||||
cuda_device: selectedCudaDevice,
|
||||
enabled: fallbackToRemote ? false : true, // Remote fallback starts disabled until configured
|
||||
extra_args: "",
|
||||
};
|
||||
|
||||
@@ -260,16 +319,20 @@ export async function addNewWorker(extension) {
|
||||
cuda_device: newWorker.cuda_device,
|
||||
extra_args: newWorker.extra_args,
|
||||
enabled: newWorker.enabled,
|
||||
host: newWorker.host,
|
||||
type: newWorker.type,
|
||||
});
|
||||
|
||||
// Sync to state
|
||||
extension.state.updateWorker(newId, { enabled: true });
|
||||
extension.state.updateWorker(newId, { enabled: newWorker.enabled });
|
||||
|
||||
extension.app.extensionManager.toast.add({
|
||||
severity: "success",
|
||||
summary: "Worker Added",
|
||||
detail: `New worker created on port ${nextPort}`,
|
||||
life: 3000,
|
||||
severity: fallbackToRemote ? "warn" : "success",
|
||||
summary: fallbackToRemote ? "Remote Worker Added" : "Worker Added",
|
||||
detail: fallbackToRemote
|
||||
? `No local GPU available, so a disabled remote worker was added on port ${nextPort}. Configure host and enable it.`
|
||||
: `New worker created on port ${nextPort}`,
|
||||
life: fallbackToRemote ? 5000 : 3000,
|
||||
});
|
||||
|
||||
// Refresh UI and expand the new worker
|
||||
|
||||
Reference in New Issue
Block a user