261 lines
13 KiB
JavaScript
261 lines
13 KiB
JavaScript
import { BUTTON_STYLES } from '../constants.js';
|
||
import { cancelWorkerSettings, deleteWorker, isRemoteWorker, saveWorkerSettings, testServerlessWorker } from '../workerSettings.js';
|
||
|
||
export const IMAGES_PER_RUN_TOOLTIP =
|
||
"Separate Runpod jobs submitted each run, all at once. Runpod's max workers decides how many GPUs actually run them.";
|
||
|
||
const MEDIA_PROFILE_TOOLTIP =
|
||
"Select Typed media v1 only after installing the ComfyUI-Distributed endpoint bridge. This declares bridge support; endpoint health checks do not auto-detect it. The v1 schema, media formats and limits are fixed.";
|
||
|
||
export function createWorkerSettingsForm(ui, extension, worker) {
|
||
const form = document.createElement("div");
|
||
form.style.cssText = "display: flex; flex-direction: column; gap: 8px;";
|
||
|
||
const nameGroup = ui.createFormGroup("Name:", worker.name, `name-${worker.id}`);
|
||
form.appendChild(nameGroup.group);
|
||
|
||
const typeGroup = document.createElement("div");
|
||
typeGroup.style.cssText = "display: flex; flex-direction: column; gap: 4px; margin: 5px 0;";
|
||
|
||
const typeLabel = document.createElement("label");
|
||
typeLabel.htmlFor = `worker-type-${worker.id}`;
|
||
typeLabel.textContent = "Worker Type:";
|
||
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: 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";
|
||
localOption.textContent = "Local";
|
||
|
||
const remoteOption = document.createElement("option");
|
||
remoteOption.value = "remote";
|
||
remoteOption.textContent = "Remote";
|
||
|
||
const cloudOption = document.createElement("option");
|
||
cloudOption.value = "cloud";
|
||
cloudOption.textContent = "Cloud";
|
||
|
||
const serverlessOption = document.createElement("option");
|
||
serverlessOption.value = "serverless";
|
||
serverlessOption.textContent = "Serverless (Runpod)";
|
||
|
||
typeSelect.appendChild(localOption);
|
||
typeSelect.appendChild(remoteOption);
|
||
typeSelect.appendChild(cloudOption);
|
||
typeSelect.appendChild(serverlessOption);
|
||
|
||
const runpodText = document.createElement("a");
|
||
runpodText.id = `runpod-text-${worker.id}`;
|
||
runpodText.href = "https://github.com/robertvoy/ComfyUI-Distributed/blob/main/docs/worker-setup-guides.md#cloud-workers";
|
||
runpodText.target = "_blank";
|
||
runpodText.textContent = "Deploy Cloud Worker with Runpod";
|
||
runpodText.style.cssText = "font-size: 12px; color: #4a90e2; text-decoration: none; margin-top: 4px; display: none; cursor: pointer;";
|
||
|
||
const updateWorkerType = (workerType, applyDefaults = false) => {
|
||
// The form is initialized before its card is attached to document.
|
||
// Use this form's own elements, not document-wide ID lookups.
|
||
const hostInput = hostGroup.input;
|
||
const portInput = portGroup.input;
|
||
const serverlessGroups = [
|
||
endpointGroup.group,
|
||
mediaProfileGroup,
|
||
apiKeyGroup.group,
|
||
clearApiKeyGroup,
|
||
jobsPerRunGroup.group,
|
||
timeoutGroup.group,
|
||
testServerlessGroup,
|
||
];
|
||
|
||
const hideServerless = () => serverlessGroups.forEach((group) => { group.style.display = "none"; });
|
||
|
||
if (workerType === "local") {
|
||
hideServerless();
|
||
hostGroup.group.style.display = "none";
|
||
portGroup.group.style.display = "flex";
|
||
cudaGroup.group.style.display = "flex";
|
||
argsGroup.group.style.display = "flex";
|
||
runpodText.style.display = "none";
|
||
} else if (workerType === "remote") {
|
||
hideServerless();
|
||
hostGroup.group.style.display = "flex";
|
||
portGroup.group.style.display = "flex";
|
||
cudaGroup.group.style.display = "none";
|
||
argsGroup.group.style.display = "none";
|
||
runpodText.style.display = "none";
|
||
hostInput.placeholder = "e.g., 192.168.1.100";
|
||
if (applyDefaults && (hostInput.value === "localhost" || hostInput.value === "127.0.0.1")) {
|
||
hostInput.value = "";
|
||
}
|
||
} else if (workerType === "cloud") {
|
||
hideServerless();
|
||
hostGroup.group.style.display = "flex";
|
||
portGroup.group.style.display = "flex";
|
||
cudaGroup.group.style.display = "none";
|
||
argsGroup.group.style.display = "none";
|
||
runpodText.style.display = "block";
|
||
hostInput.placeholder = "e.g., your-cloud-worker.trycloudflare.com";
|
||
if (applyDefaults) portInput.value = "443";
|
||
if (applyDefaults && (hostInput.value === "localhost" || hostInput.value === "127.0.0.1")) {
|
||
hostInput.value = "";
|
||
}
|
||
} else if (workerType === "serverless") {
|
||
hostGroup.group.style.display = "none";
|
||
portGroup.group.style.display = "none";
|
||
cudaGroup.group.style.display = "none";
|
||
argsGroup.group.style.display = "none";
|
||
runpodText.style.display = "none";
|
||
serverlessGroups.forEach((group) => { group.style.display = "flex"; });
|
||
}
|
||
};
|
||
|
||
typeGroup.appendChild(typeLabel);
|
||
typeGroup.appendChild(typeSelect);
|
||
typeGroup.appendChild(runpodText);
|
||
form.appendChild(typeGroup);
|
||
|
||
const hostGroup = ui.createFormGroup("Host:", worker.host || "", `host-${worker.id}`, "text", "e.g., 192.168.1.100");
|
||
hostGroup.group.id = `host-group-${worker.id}`;
|
||
hostGroup.group.style.display = (isRemoteWorker(extension, worker) || worker.type === "cloud") ? "flex" : "none";
|
||
form.appendChild(hostGroup.group);
|
||
|
||
const portGroup = ui.createFormGroup("Port:", worker.port, `port-${worker.id}`, "number");
|
||
portGroup.group.id = `port-group-${worker.id}`;
|
||
form.appendChild(portGroup.group);
|
||
|
||
const cudaGroup = ui.createFormGroup("CUDA Device:", worker.cuda_device || 0, `cuda-${worker.id}`, "number");
|
||
cudaGroup.group.id = `cuda-group-${worker.id}`;
|
||
cudaGroup.group.style.display = (isRemoteWorker(extension, worker) || worker.type === "cloud") ? "none" : "flex";
|
||
form.appendChild(cudaGroup.group);
|
||
|
||
const argsGroup = ui.createFormGroup("Extra Args:", worker.extra_args || "", `args-${worker.id}`);
|
||
argsGroup.group.id = `args-group-${worker.id}`;
|
||
argsGroup.group.style.display = (isRemoteWorker(extension, worker) || worker.type === "cloud") ? "none" : "flex";
|
||
form.appendChild(argsGroup.group);
|
||
|
||
const endpointGroup = ui.createFormGroup("Endpoint ID:", worker.endpoint_id || "", `endpoint-id-${worker.id}`, "text", "Runpod endpoint ID");
|
||
endpointGroup.group.id = `endpoint-id-group-${worker.id}`;
|
||
endpointGroup.group.style.display = "none";
|
||
form.appendChild(endpointGroup.group);
|
||
|
||
const mediaProfileGroup = document.createElement("div");
|
||
mediaProfileGroup.id = `media-profile-group-${worker.id}`;
|
||
mediaProfileGroup.style.cssText = "display: none; flex-direction: column; gap: 4px;";
|
||
mediaProfileGroup.title = MEDIA_PROFILE_TOOLTIP;
|
||
const mediaProfileLabel = document.createElement("label");
|
||
mediaProfileLabel.htmlFor = `media-profile-${worker.id}`;
|
||
mediaProfileLabel.textContent = "Endpoint media profile";
|
||
mediaProfileLabel.style.cssText = typeLabel.style.cssText;
|
||
const mediaProfileSelect = document.createElement("select");
|
||
mediaProfileSelect.id = mediaProfileLabel.htmlFor;
|
||
mediaProfileSelect.style.cssText = typeSelect.style.cssText;
|
||
mediaProfileSelect.title = MEDIA_PROFILE_TOOLTIP;
|
||
for (const [value, label] of [
|
||
["legacy-image-v1", "Legacy images (existing handler)"],
|
||
["comfy-media-v1", "Typed media v1 (requires endpoint bridge)"],
|
||
]) {
|
||
const option = document.createElement("option");
|
||
option.value = value;
|
||
option.textContent = label;
|
||
mediaProfileSelect.appendChild(option);
|
||
}
|
||
mediaProfileSelect.value = worker.media_profile ?? "legacy-image-v1";
|
||
mediaProfileGroup.appendChild(mediaProfileLabel);
|
||
mediaProfileGroup.appendChild(mediaProfileSelect);
|
||
form.appendChild(mediaProfileGroup);
|
||
|
||
const apiKeyGroup = ui.createFormGroup("Runpod API Key (write-only):", "", `api-key-${worker.id}`, "password", worker.api_key_configured ? "Leave blank to keep saved API key" : "Runpod API key");
|
||
apiKeyGroup.group.id = `api-key-group-${worker.id}`;
|
||
apiKeyGroup.input.autocomplete = "new-password";
|
||
apiKeyGroup.input.spellcheck = false;
|
||
apiKeyGroup.group.style.display = "none";
|
||
form.appendChild(apiKeyGroup.group);
|
||
|
||
const clearApiKeyGroup = document.createElement("div");
|
||
clearApiKeyGroup.id = `clear-api-key-group-${worker.id}`;
|
||
clearApiKeyGroup.style.cssText = "display: none; align-items: center; gap: 8px;";
|
||
const clearApiKeyInput = document.createElement("input");
|
||
clearApiKeyInput.type = "checkbox";
|
||
clearApiKeyInput.id = `clear-api-key-${worker.id}`;
|
||
const clearApiKeyLabel = document.createElement("label");
|
||
clearApiKeyLabel.htmlFor = clearApiKeyInput.id;
|
||
clearApiKeyLabel.textContent = "Clear saved API key";
|
||
// The backend rejects clearing the key of an enabled worker; say so up front.
|
||
// The form is rebuilt when the enable toggle changes, so this stays current.
|
||
const clearApiKeyBlockedReason = !worker.api_key_configured
|
||
? "No API key is saved for this worker."
|
||
: worker.enabled
|
||
? "Turn this worker off before clearing its API key. An enabled serverless worker needs a saved key."
|
||
: "";
|
||
clearApiKeyInput.disabled = Boolean(clearApiKeyBlockedReason);
|
||
clearApiKeyGroup.title = clearApiKeyBlockedReason || "Remove the saved Runpod API key when you save.";
|
||
clearApiKeyLabel.style.cssText = clearApiKeyBlockedReason ? "opacity: 0.5; cursor: not-allowed;" : "cursor: pointer;";
|
||
clearApiKeyGroup.appendChild(clearApiKeyInput);
|
||
clearApiKeyGroup.appendChild(clearApiKeyLabel);
|
||
clearApiKeyInput.onchange = () => {
|
||
if (clearApiKeyInput.checked) apiKeyGroup.input.value = "";
|
||
apiKeyGroup.input.disabled = clearApiKeyInput.checked;
|
||
};
|
||
form.appendChild(clearApiKeyGroup);
|
||
|
||
const jobsPerRunGroup = ui.createFormGroup("Images per run (1–16):", worker.jobs_per_run ?? 1, `jobs-per-run-${worker.id}`, "number");
|
||
jobsPerRunGroup.group.id = `jobs-per-run-group-${worker.id}`;
|
||
jobsPerRunGroup.input.min = "1";
|
||
jobsPerRunGroup.input.max = "16";
|
||
jobsPerRunGroup.input.step = "1";
|
||
jobsPerRunGroup.input.title = IMAGES_PER_RUN_TOOLTIP;
|
||
jobsPerRunGroup.group.title = IMAGES_PER_RUN_TOOLTIP;
|
||
jobsPerRunGroup.group.style.display = "none";
|
||
form.appendChild(jobsPerRunGroup.group);
|
||
|
||
const timeoutGroup = ui.createFormGroup("Timeout (seconds):", worker.timeout_seconds ?? 1800, `timeout-seconds-${worker.id}`, "number");
|
||
timeoutGroup.group.id = `timeout-seconds-group-${worker.id}`;
|
||
timeoutGroup.input.min = "30";
|
||
timeoutGroup.input.max = "86400";
|
||
timeoutGroup.input.step = "1";
|
||
timeoutGroup.group.style.display = "none";
|
||
form.appendChild(timeoutGroup.group);
|
||
|
||
const testServerlessButton = ui.createButton("Test Saved Endpoint", () => testServerlessWorker(extension, worker.id));
|
||
testServerlessButton.id = `test-serverless-${worker.id}`;
|
||
testServerlessButton.title = "Test the saved Runpod endpoint without submitting inference";
|
||
testServerlessButton.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
|
||
const testServerlessGroup = document.createElement("div");
|
||
testServerlessGroup.id = `serverless-test-group-${worker.id}`;
|
||
testServerlessGroup.style.cssText = "display: none;";
|
||
testServerlessGroup.appendChild(testServerlessButton);
|
||
form.appendChild(testServerlessGroup);
|
||
|
||
const saveBtn = ui.createButton("Save", () => saveWorkerSettings(extension, worker.id), "background-color: #4a7c4a;");
|
||
saveBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.success;
|
||
|
||
const cancelBtn = ui.createButton("Cancel", () => cancelWorkerSettings(extension, worker.id), "background-color: #555;");
|
||
cancelBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.cancel;
|
||
|
||
const deleteBtn = ui.createButton("Delete", () => deleteWorker(extension, worker.id), "background-color: #7c4a4a;");
|
||
deleteBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.error + BUTTON_STYLES.marginLeftAuto;
|
||
|
||
const buttonGroup = ui.createButtonGroup([saveBtn, cancelBtn, deleteBtn], " margin-top: 8px;");
|
||
form.appendChild(buttonGroup);
|
||
|
||
typeSelect.onchange = (event) => updateWorkerType(event.target.value, true);
|
||
|
||
if (worker.type === "serverless") {
|
||
typeSelect.value = "serverless";
|
||
} else if (worker.type === "cloud") {
|
||
typeSelect.value = "cloud";
|
||
runpodText.style.display = "block";
|
||
} else if (isRemoteWorker(extension, worker)) {
|
||
typeSelect.value = "remote";
|
||
} else {
|
||
typeSelect.value = "local";
|
||
}
|
||
|
||
updateWorkerType(typeSelect.value);
|
||
|
||
return form;
|
||
}
|