mirror of
https://git.datalinker.icu/ltdrdata/ComfyUI-Manager
synced 2025-12-09 22:24:23 +08:00
Merge pull request #308 from aplex/new-workflow-sharing-option
Add new workflow sharing option
This commit is contained in:
commit
9891418469
@ -133,7 +133,9 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
 
|
||||
|
||||
* You can share the workflow by clicking the Share button at the bottom of the main menu or selecting Share Output from the Context Menu of the Image node.
|
||||
* Currently, it supports sharing via [https://comfyworkflows.com/](https://comfyworkflows.com/) and [https://openart.ai](https://openart.ai/workflows/dev), as well as through the Matrix channel.
|
||||
* Currently, it supports sharing via [https://comfyworkflows.com/](https://comfyworkflows.com/),
|
||||
[https://openart.ai](https://openart.ai/workflows/dev), [https://youml.com](https://youml.com)
|
||||
as well as through the Matrix channel.
|
||||
|
||||

|
||||
|
||||
|
||||
31
__init__.py
31
__init__.py
@ -1874,6 +1874,22 @@ def get_comfyworkflows_auth():
|
||||
return None
|
||||
|
||||
|
||||
def get_youml_settings():
|
||||
if not os.path.exists(os.path.join(comfyui_manager_path, ".youml")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(comfyui_manager_path, ".youml"), "r") as f:
|
||||
youml_settings = f.read().strip()
|
||||
return youml_settings if youml_settings else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def set_youml_settings(settings):
|
||||
with open(os.path.join(comfyui_manager_path, ".youml"), "w") as f:
|
||||
f.write(settings)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/get_openart_auth")
|
||||
async def api_get_openart_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
@ -1901,6 +1917,21 @@ async def api_get_matrix_auth(request):
|
||||
return web.json_response(matrix_auth)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/youml/settings")
|
||||
async def api_get_youml_settings(request):
|
||||
youml_settings = get_youml_settings()
|
||||
if not youml_settings:
|
||||
return web.Response(status=404)
|
||||
return web.json_response(json.loads(youml_settings))
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/manager/youml/settings")
|
||||
async def api_set_youml_settings(request):
|
||||
json_data = await request.json()
|
||||
set_youml_settings(json.dumps(json_data))
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth")
|
||||
async def api_get_comfyworkflows_auth(request):
|
||||
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { ShareDialog, SUPPORTED_OUTPUT_NODE_TYPES, getPotentialOutputsAndOutputNodes, ShareDialogChooser, showOpenArtShareDialog, showShareDialog } from "./comfyui-share-common.js";
|
||||
import {
|
||||
ShareDialog,
|
||||
SUPPORTED_OUTPUT_NODE_TYPES,
|
||||
getPotentialOutputsAndOutputNodes,
|
||||
ShareDialogChooser,
|
||||
showOpenArtShareDialog,
|
||||
showShareDialog,
|
||||
showYouMLShareDialog
|
||||
} from "./comfyui-share-common.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { CustomNodesInstaller } from "./custom-nodes-downloader.js";
|
||||
import { AlternativesInstaller } from "./a1111-alter-downloader.js";
|
||||
@ -734,6 +742,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
const share_options = [
|
||||
['none', 'None'],
|
||||
['openart', 'OpenArt AI'],
|
||||
['youml', 'YouML'],
|
||||
['matrix', 'Matrix Server'],
|
||||
['comfyworkflows', 'ComfyWorkflows'],
|
||||
['all', 'All'],
|
||||
@ -985,6 +994,9 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
} else if (share_option === 'youml') {
|
||||
showYouMLShareDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShareDialogChooser.instance) {
|
||||
@ -1002,6 +1014,15 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
modifyButtonStyle(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'youml.com'",
|
||||
callback: () => {
|
||||
const url = "https://youml.com/?from=comfyui-share";
|
||||
localStorage.setItem("wg_last_visited", url);
|
||||
window.open(url, "comfyui-workflow-gallery");
|
||||
modifyButtonStyle(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Open 'comfyworkflows.com'",
|
||||
callback: () => {
|
||||
@ -1067,6 +1088,9 @@ app.registerExtension({
|
||||
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') {
|
||||
showShareDialog(share_option);
|
||||
return;
|
||||
} else if (share_option === 'youml') {
|
||||
showYouMLShareDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ShareDialogChooser.instance) {
|
||||
|
||||
@ -2,6 +2,7 @@ import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { YouMLShareDialog } from "./comfyui-share-youml.js";
|
||||
|
||||
export const SUPPORTED_OUTPUT_NODE_TYPES = [
|
||||
"PreviewImage",
|
||||
@ -179,6 +180,23 @@ export const showOpenArtShareDialog = () => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const showYouMLShareDialog = () => {
|
||||
if (!YouMLShareDialog.instance) {
|
||||
YouMLShareDialog.instance = new YouMLShareDialog();
|
||||
}
|
||||
|
||||
return app.graphToPrompt()
|
||||
.then(prompt => {
|
||||
return app.graph._nodes;
|
||||
})
|
||||
.then(nodes => {
|
||||
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
|
||||
YouMLShareDialog.instance.show(potential_outputs, potential_output_nodes);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const showShareDialog = async (share_option) => {
|
||||
if (!ShareDialog.instance) {
|
||||
ShareDialog.instance = new ShareDialog(share_option);
|
||||
@ -233,6 +251,16 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "youml",
|
||||
textContent: "YouML",
|
||||
website: "https://youml.com",
|
||||
description: "Share your workflow or transform it into an interactive app on YouML.com",
|
||||
onclick: () => {
|
||||
showYouMLShareDialog();
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "matrix",
|
||||
textContent: "Matrix Server",
|
||||
@ -264,7 +292,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
display: "flex",
|
||||
'flex-wrap': 'wrap',
|
||||
'justify-content': 'space-around',
|
||||
'padding': '20px',
|
||||
'padding': '10px',
|
||||
}
|
||||
});
|
||||
|
||||
@ -297,7 +325,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
'text-align': 'left',
|
||||
color: 'white',
|
||||
'font-size': '14px',
|
||||
'margin-bottom': '10px',
|
||||
'margin-bottom': '0',
|
||||
},
|
||||
});
|
||||
|
||||
@ -335,7 +363,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
style: {
|
||||
'flex-basis': '100%',
|
||||
'margin': '10px',
|
||||
'padding': '20px',
|
||||
'padding': '10px 20px',
|
||||
'border': '1px solid #ddd',
|
||||
'border-radius': '5px',
|
||||
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
|
||||
568
js/comfyui-share-youml.js
Normal file
568
js/comfyui-share-youml.js
Normal file
@ -0,0 +1,568 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {api} from "../../scripts/api.js";
|
||||
import {ComfyDialog, $el} from "../../scripts/ui.js";
|
||||
|
||||
const BASE_URL = "https://youml.com";
|
||||
//const BASE_URL = "http://localhost:3000";
|
||||
const DEFAULT_HOMEPAGE_URL = `${BASE_URL}/?from=comfyui`;
|
||||
const TOKEN_PAGE_URL = `${BASE_URL}/my-token`;
|
||||
const API_ENDPOINT = `${BASE_URL}/api`;
|
||||
|
||||
const style = `
|
||||
.youml-share-dialog {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.youml-share-dialog .dialog-header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.youml-share-dialog .dialog-section {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.youml-share-dialog input, .youml-share-dialog textarea {
|
||||
display: block;
|
||||
min-width: 500px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.youml-share-dialog textarea {
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
.youml-share-dialog .workflow-description {
|
||||
min-height: 75px;
|
||||
}
|
||||
.youml-share-dialog label {
|
||||
color: #f8f8f8;
|
||||
display: block;
|
||||
margin: 5px 0 0 0;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
.youml-share-dialog .action-button {
|
||||
padding: 10px 80px;
|
||||
margin: 10px 5px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.youml-share-dialog .share-button {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
}
|
||||
.youml-share-dialog .close-button {
|
||||
background-color: none;
|
||||
}
|
||||
.youml-share-dialog .action-button-panel {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.youml-share-dialog .status-message {
|
||||
color: #fd7909;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.youml-share-dialog .status-message a {
|
||||
color: white;
|
||||
}
|
||||
.youml-share-dialog .output-panel {
|
||||
overflow: auto;
|
||||
max-height: 180px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
grid-template-rows: auto;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
objectFit: cover;
|
||||
borderRadius: 5px;
|
||||
}
|
||||
|
||||
.youml-share-dialog .output-panel .radio-button {
|
||||
color:var(--fg-color);
|
||||
}
|
||||
.youml-share-dialog .output-panel .radio-text {
|
||||
color: gray;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
max-width: 100px;
|
||||
}
|
||||
.youml-share-dialog .output-panel .node-id {
|
||||
color: #FBFBFD;
|
||||
display: block;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
padding: 2px 3px;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
max-width: 100px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label:hover {
|
||||
border: 5px solid #007bff;
|
||||
}
|
||||
.youml-share-dialog .output-panel .output-label.checked {
|
||||
border: 5px solid #007bff;
|
||||
}
|
||||
.youml-share-dialog .missing-output-message{
|
||||
color: #fd7909;
|
||||
font-size: 16px;
|
||||
margin-bottom:10px
|
||||
}
|
||||
.youml-share-dialog .select-output-message{
|
||||
color: white;
|
||||
margin-bottom:5px
|
||||
}
|
||||
`;
|
||||
|
||||
export class YouMLShareDialog extends ComfyDialog {
|
||||
static instance = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
$el("style", {
|
||||
textContent: style,
|
||||
parent: document.head,
|
||||
});
|
||||
this.element = $el(
|
||||
"div.comfy-modal.youml-share-dialog",
|
||||
{
|
||||
parent: document.body,
|
||||
},
|
||||
[$el("div.comfy-modal-content", {}, [...this.createLayout()])]
|
||||
);
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.uploadedImages = [];
|
||||
this.selectedFile = null;
|
||||
}
|
||||
|
||||
async loadToken() {
|
||||
let key = ""
|
||||
try {
|
||||
const response = await api.fetchApi(`/manager/youml/settings`)
|
||||
const settings = await response.json()
|
||||
return settings.token
|
||||
} catch (error) {
|
||||
}
|
||||
return key || "";
|
||||
}
|
||||
|
||||
async saveToken(value) {
|
||||
await api.fetchApi(`/manager/youml/settings`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
token: value
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
createLayout() {
|
||||
// Header Section
|
||||
const headerSection = $el("h3.dialog-header", {
|
||||
textContent: "Share your workflow to YouML.com",
|
||||
size: 3,
|
||||
});
|
||||
|
||||
// Workflow Info Section
|
||||
this.nameInput = $el("input", {
|
||||
type: "text",
|
||||
placeholder: "Name (required)",
|
||||
});
|
||||
this.descriptionInput = $el("textarea.workflow-description", {
|
||||
placeholder: "Description (optional, markdown supported)",
|
||||
});
|
||||
const workflowMetadata = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["Workflow info"]),
|
||||
this.nameInput,
|
||||
this.descriptionInput,
|
||||
]);
|
||||
|
||||
// Outputs Section
|
||||
this.outputsSection = $el("div.dialog-section", {
|
||||
id: "selectOutputs",
|
||||
}, []);
|
||||
|
||||
const outputUploadSection = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["Thumbnail"]),
|
||||
this.outputsSection,
|
||||
]);
|
||||
|
||||
// API Token Section
|
||||
this.apiTokenInput = $el("input", {
|
||||
type: "password",
|
||||
placeholder: "Copy & paste your API token",
|
||||
});
|
||||
const getAPITokenButton = $el("button", {
|
||||
href: DEFAULT_HOMEPAGE_URL,
|
||||
target: "_blank",
|
||||
onclick: () => window.open(TOKEN_PAGE_URL, "_blank"),
|
||||
}, ["Get your API Token"])
|
||||
|
||||
const apiTokenSection = $el("div.dialog-section", {}, [
|
||||
$el("label", {}, ["YouML API Token"]),
|
||||
this.apiTokenInput,
|
||||
getAPITokenButton,
|
||||
]);
|
||||
|
||||
// Message Section
|
||||
this.message = $el("div.status-message", {}, []);
|
||||
|
||||
// Share and Close Buttons
|
||||
this.shareButton = $el("button.action-button.share-button", {
|
||||
type: "submit",
|
||||
textContent: "Share",
|
||||
onclick: () => {
|
||||
this.handleShareButtonClick();
|
||||
},
|
||||
});
|
||||
|
||||
const buttonsSection = $el(
|
||||
"div.action-button-panel",
|
||||
{},
|
||||
[
|
||||
$el("button.action-button.close-button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => {
|
||||
this.close();
|
||||
},
|
||||
}),
|
||||
this.shareButton,
|
||||
]
|
||||
);
|
||||
|
||||
// Composing the full layout
|
||||
const layout = [
|
||||
headerSection,
|
||||
workflowMetadata,
|
||||
outputUploadSection,
|
||||
apiTokenSection,
|
||||
this.message,
|
||||
buttonsSection,
|
||||
];
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
async fetchYoumlApi(path, options, statusText) {
|
||||
if (statusText) {
|
||||
this.message.textContent = statusText;
|
||||
}
|
||||
|
||||
const fullPath = new URL(API_ENDPOINT + path)
|
||||
|
||||
const fetchOptions = Object.assign({}, options)
|
||||
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
"Authorization": `Bearer ${this.apiTokenInput.value}`,
|
||||
"User-Agent": "ComfyUI-Manager-Youml/1.0.0",
|
||||
}
|
||||
|
||||
const response = await fetch(fullPath, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText + " " + (await response.text()));
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
this.message.textContent = "";
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
ok: response.ok,
|
||||
statusText: response.statusText,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadThumbnail(uploadFile, recipeId) {
|
||||
const form = new FormData();
|
||||
form.append("file", uploadFile, uploadFile.name);
|
||||
try {
|
||||
const res = await this.fetchYoumlApi(
|
||||
`/v1/comfy/recipes/${recipeId}/thumbnail`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
},
|
||||
"Uploading thumbnail..."
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 413) {
|
||||
throw new Error("File size is too large (max 20MB)");
|
||||
} else {
|
||||
throw new Error("Error uploading thumbnail: " + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleShareButtonClick() {
|
||||
this.message.textContent = "";
|
||||
await this.saveToken(this.apiTokenInput.value);
|
||||
try {
|
||||
this.shareButton.disabled = true;
|
||||
this.shareButton.textContent = "Sharing...";
|
||||
await this.share();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
this.shareButton.disabled = false;
|
||||
this.shareButton.textContent = "Share";
|
||||
}
|
||||
}
|
||||
|
||||
async share() {
|
||||
const prompt = await app.graphToPrompt();
|
||||
const workflowJSON = prompt["workflow"];
|
||||
const workflowAPIJSON = prompt["output"];
|
||||
const form_values = {
|
||||
name: this.nameInput.value,
|
||||
description: this.descriptionInput.value,
|
||||
};
|
||||
|
||||
if (!this.apiTokenInput.value) {
|
||||
throw new Error("API token is required");
|
||||
}
|
||||
|
||||
if (!this.selectedFile) {
|
||||
throw new Error("Thumbnail is required");
|
||||
}
|
||||
|
||||
if (!form_values.name) {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
let snapshotData = null;
|
||||
try {
|
||||
const snapshot = await api.fetchApi(`/snapshot/get_current`)
|
||||
snapshotData = await snapshot.json()
|
||||
} catch (e) {
|
||||
console.error("Failed to get snapshot", e)
|
||||
}
|
||||
|
||||
const request = {
|
||||
name: this.nameInput.value,
|
||||
description: this.descriptionInput.value,
|
||||
workflowUiJson: JSON.stringify(workflowJSON),
|
||||
workflowApiJson: JSON.stringify(workflowAPIJSON),
|
||||
}
|
||||
|
||||
if (snapshotData) {
|
||||
request.snapshotJson = JSON.stringify(snapshotData)
|
||||
}
|
||||
|
||||
const response = await this.fetchYoumlApi(
|
||||
"/v1/comfy/recipes",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(request),
|
||||
},
|
||||
"Uploading workflow..."
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const {id, recipePageUrl, editorPageUrl} = response.data;
|
||||
if (id) {
|
||||
let messagePrefix = "Workflow has been shared."
|
||||
if (this.selectedFile) {
|
||||
try {
|
||||
await this.uploadThumbnail(this.selectedFile, id);
|
||||
} catch (e) {
|
||||
console.error("Thumbnail upload failed: ", e);
|
||||
messagePrefix = "Workflow has been shared, but thumbnail upload failed. You can create a thumbnail on YouML later."
|
||||
}
|
||||
}
|
||||
this.message.innerHTML = `${messagePrefix} To turn your workflow into an interactive app, ` +
|
||||
`<a href="${recipePageUrl}" target="_blank">visit it on YouML</a>`;
|
||||
|
||||
this.uploadedImages = [];
|
||||
this.nameInput.value = "";
|
||||
this.descriptionInput.value = "";
|
||||
this.radioButtons.forEach((ele) => {
|
||||
ele.checked = false;
|
||||
ele.parentElement.classList.remove("checked");
|
||||
});
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Error sharing workflow: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchImageBlob(url) {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return blob;
|
||||
}
|
||||
|
||||
async show(potentialOutputs, potentialOutputNodes) {
|
||||
const potentialOutputsToOrder = {};
|
||||
potentialOutputNodes.forEach((node, index) => {
|
||||
if (node.id in potentialOutputsToOrder) {
|
||||
potentialOutputsToOrder[node.id][1].push(potentialOutputs[index]);
|
||||
} else {
|
||||
potentialOutputsToOrder[node.id] = [node, [potentialOutputs[index]]];
|
||||
}
|
||||
})
|
||||
const sortedPotentialOutputsToOrder = Object.fromEntries(
|
||||
Object.entries(potentialOutputsToOrder).sort((a, b) => a[0].id - b[0].id)
|
||||
);
|
||||
const sortedPotentialOutputs = []
|
||||
const sortedPotentiaOutputNodes = []
|
||||
for (const [key, value] of Object.entries(sortedPotentialOutputsToOrder)) {
|
||||
sortedPotentiaOutputNodes.push(value[0]);
|
||||
sortedPotentialOutputs.push(...value[1]);
|
||||
}
|
||||
potentialOutputNodes = sortedPotentiaOutputNodes;
|
||||
potentialOutputs = sortedPotentialOutputs;
|
||||
|
||||
|
||||
// If `selectedNodeId` is provided, we will select the corresponding radio
|
||||
// button for the node. In addition, we move the selected radio button to
|
||||
// the top of the list.
|
||||
if (this.selectedNodeId) {
|
||||
const index = potentialOutputNodes.findIndex(node => node.id === this.selectedNodeId);
|
||||
if (index >= 0) {
|
||||
this.selectedOutputIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
this.radioButtons = [];
|
||||
const newRadioButtons = $el("div.output-panel",
|
||||
{
|
||||
id: "selectOutput-Options",
|
||||
},
|
||||
potentialOutputs.map((output, index) => {
|
||||
const {node_id: nodeId} = output;
|
||||
const radioButton = $el("input.radio-button", {
|
||||
type: "radio",
|
||||
name: "selectOutputImages",
|
||||
value: index,
|
||||
required: index === 0
|
||||
}, [])
|
||||
let radioButtonImage;
|
||||
let filename;
|
||||
if (output.type === "image" || output.type === "temp") {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`,
|
||||
}, []);
|
||||
filename = output.image.filename
|
||||
} else if (output.type === "output") {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: output.output.value,
|
||||
}, []);
|
||||
filename = output.output.filename
|
||||
} else {
|
||||
radioButtonImage = $el("img.output-image", {
|
||||
src: "",
|
||||
}, []);
|
||||
}
|
||||
const radioButtonText = $el("span.radio-text", {}, [output.title])
|
||||
const nodeIdChip = $el("span.node-id", {}, [`Node: ${nodeId}`])
|
||||
radioButton.checked = this.selectedOutputIndex === index;
|
||||
|
||||
radioButton.onchange = async () => {
|
||||
this.selectedOutputIndex = parseInt(radioButton.value);
|
||||
|
||||
// Remove the "checked" class from all radio buttons
|
||||
this.radioButtons.forEach((ele) => {
|
||||
ele.parentElement.classList.remove("checked");
|
||||
});
|
||||
radioButton.parentElement.classList.add("checked");
|
||||
|
||||
this.fetchImageBlob(radioButtonImage.src).then((blob) => {
|
||||
const file = new File([blob], filename, {
|
||||
type: blob.type,
|
||||
});
|
||||
this.selectedFile = file;
|
||||
})
|
||||
};
|
||||
|
||||
if (radioButton.checked) {
|
||||
this.fetchImageBlob(radioButtonImage.src).then((blob) => {
|
||||
const file = new File([blob], filename, {
|
||||
type: blob.type,
|
||||
});
|
||||
this.selectedFile = file;
|
||||
})
|
||||
}
|
||||
|
||||
this.radioButtons.push(radioButton);
|
||||
|
||||
return $el(`label.output-label${radioButton.checked ? '.checked' : ''}`, {},
|
||||
[radioButtonImage, radioButtonText, radioButton, nodeIdChip]);
|
||||
})
|
||||
);
|
||||
|
||||
let header;
|
||||
if (this.radioButtons.length === 0) {
|
||||
header = $el("div.missing-output-message", {textContent: "Queue Prompt to see the outputs and select a thumbnail"}, [])
|
||||
} else {
|
||||
header = $el("div.select-output-message", {textContent: "Choose one from the outputs (scroll to see all)"}, [])
|
||||
}
|
||||
|
||||
this.outputsSection.innerHTML = "";
|
||||
this.outputsSection.appendChild(header);
|
||||
if (this.radioButtons.length > 0) {
|
||||
this.outputsSection.appendChild(newRadioButtons);
|
||||
}
|
||||
|
||||
this.message.innerHTML = "";
|
||||
this.message.textContent = "";
|
||||
|
||||
const token = await this.loadToken();
|
||||
this.apiTokenInput.value = token;
|
||||
this.uploadedImages = [];
|
||||
|
||||
this.element.style.display = "block";
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user