diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js
index 4ad3999e..1390513a 100644
--- a/js/comfyui-manager.js
+++ b/js/comfyui-manager.js
@@ -1,22 +1,22 @@
+import { api } from "../../scripts/api.js";
import { app } from "../../scripts/app.js";
-import { api } from "../../scripts/api.js"
-import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { $el, ComfyDialog } from "../../scripts/ui.js";
import {
- ShareDialog,
SUPPORTED_OUTPUT_NODE_TYPES,
- getPotentialOutputsAndOutputNodes,
+ ShareDialog,
ShareDialogChooser,
+ getPotentialOutputsAndOutputNodes,
showOpenArtShareDialog,
showShareDialog,
showYouMLShareDialog
} from "./comfyui-share-common.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
+import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message } from "./common.js";
+import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
-import { SnapshotManager } from "./snapshot.js";
-import { manager_instance, setManagerInstance, install_via_git_url, install_pip, rebootAPI, free_models, show_message } from "./common.js";
-import { ComponentBuilderDialog, load_components, set_component_policy, getPureName } from "./components-manager.js";
import { set_double_click_policy } from "./node_fixer.js";
+import { SnapshotManager } from "./snapshot.js";
var docStyle = document.createElement('style');
docStyle.innerHTML = `
@@ -897,6 +897,7 @@ class ManagerMenuDialog extends ComfyDialog {
['youml', 'YouML'],
['matrix', 'Matrix Server'],
['comfyworkflows', 'ComfyWorkflows'],
+ ['copus', 'Copus'],
['all', 'All'],
];
for (const option of share_options) {
@@ -1234,6 +1235,15 @@ class ManagerMenuDialog extends ComfyDialog {
modifyButtonStyle(url);
},
},
+ {
+ title: "Open 'Copus.io'",
+ callback: () => {
+ const url = "https://www.copus.io";
+ localStorage.setItem("wg_last_visited", url);
+ window.open(url, url);
+ modifyButtonStyle(url);
+ },
+ },
{
title: "Close",
callback: () => {
diff --git a/js/comfyui-share-common.js b/js/comfyui-share-common.js
index ca09dd76..1fbf0485 100644
--- a/js/comfyui-share-common.js
+++ b/js/comfyui-share-common.js
@@ -1,6 +1,7 @@
-import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
-import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { app } from "../../scripts/app.js";
+import { $el, ComfyDialog } from "../../scripts/ui.js";
+import { CopusShareDialog } from "./comfyui-share-copus.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import { YouMLShareDialog } from "./comfyui-share-youml.js";
@@ -187,6 +188,21 @@ export const shareToEsheep= () => {
})
}
+export const showCopusShareDialog = () => {
+ if (!CopusShareDialog.instance) {
+ CopusShareDialog.instance = new CopusShareDialog();
+ }
+
+ return app.graphToPrompt()
+ .then(prompt => {
+ return app.graph._nodes;
+ })
+ .then(nodes => {
+ const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
+ CopusShareDialog.instance.show({ potential_outputs, potential_output_nodes});
+ })
+}
+
export const showOpenArtShareDialog = () => {
if (!OpenArtShareDialog.instance) {
OpenArtShareDialog.instance = new OpenArtShareDialog();
@@ -316,6 +332,16 @@ export class ShareDialogChooser extends ComfyDialog {
this.close();
}
},
+ {
+ key: "Copus",
+ textContent: "Copus",
+ website: "https://www.copus.io",
+ description: "🔴 Permanently store and secure ownership of your workflow on the open-source platform: Copus.io",
+ onclick: () => {
+ showCopusShareDialog();
+ this.close();
+ }
+ },
];
function createShareButtonsWithDescriptions() {
diff --git a/js/comfyui-share-copus.js b/js/comfyui-share-copus.js
new file mode 100644
index 00000000..318b1fe5
--- /dev/null
+++ b/js/comfyui-share-copus.js
@@ -0,0 +1,892 @@
+import { app } from "../../scripts/app.js";
+import { $el, ComfyDialog } from "../../scripts/ui.js";
+const env = "prod";
+
+let DEFAULT_HOMEPAGE_URL = "https://copus.io";
+
+let API_ENDPOINT = "https://api.client.prod.copus.io/copus-client";
+
+if (env !== "prod") {
+ API_ENDPOINT = "https://api.dev.copus.io/copus-client";
+ DEFAULT_HOMEPAGE_URL = "https://test.copus.io";
+}
+
+const style = `
+ .copus-share-dialog a {
+ color: #f8f8f8;
+ }
+ .copus-share-dialog a:hover {
+ color: #007bff;
+ }
+ .output_label {
+ border: 5px solid transparent;
+ }
+ .output_label:hover {
+ border: 5px solid #59E8C6;
+ }
+ .output_label.checked {
+ border: 5px solid #59E8C6;
+ }
+`;
+
+// Shared component styles
+const sectionStyle = {
+ marginBottom: 0,
+ padding: 0,
+ borderRadius: "8px",
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ position: "relative",
+};
+
+export class CopusShareDialog extends ComfyDialog {
+ static instance = null;
+
+ constructor() {
+ super();
+ $el("style", {
+ textContent: style,
+ parent: document.head,
+ });
+ this.element = $el(
+ "div.comfy-modal.copus-share-dialog",
+ {
+ parent: document.body,
+ style: {
+ "overflow-y": "auto",
+ },
+ },
+ [$el("div.comfy-modal-content", {}, [...this.createButtons()])]
+ );
+ this.selectedOutputIndex = 0;
+ this.selectedNodeId = null;
+ this.uploadedImages = [];
+ this.allFilesImages = [];
+ this.selectedFile = null;
+ this.allFiles = [];
+ this.titleNum = 0;
+ }
+
+ createButtons() {
+ const inputStyle = {
+ display: "block",
+ minWidth: "500px",
+ width: "100%",
+ padding: "10px",
+ margin: "10px 0",
+ borderRadius: "4px",
+ border: "1px solid #ddd",
+ boxSizing: "border-box",
+ };
+
+ const textAreaStyle = {
+ display: "block",
+ minWidth: "500px",
+ width: "100%",
+ padding: "10px",
+ margin: "10px 0",
+ borderRadius: "4px",
+ border: "1px solid #ddd",
+ boxSizing: "border-box",
+ minHeight: "100px",
+ background: "#222",
+ resize: "vertical",
+ color: "#f2f2f2",
+ fontFamily: "Arial",
+ fontWeight: "400",
+ fontSize: "15px",
+ };
+
+ const hyperLinkStyle = {
+ display: "block",
+ marginBottom: "15px",
+ fontWeight: "bold",
+ fontSize: "14px",
+ };
+
+ const labelStyle = {
+ color: "#f8f8f8",
+ display: "block",
+ margin: "10px 0 0 0",
+ fontWeight: "bold",
+ textDecoration: "none",
+ };
+
+ const buttonStyle = {
+ padding: "10px 80px",
+ margin: "10px 5px",
+ borderRadius: "4px",
+ border: "none",
+ cursor: "pointer",
+ color: "#fff",
+ backgroundColor: "#007bff",
+ };
+
+ // upload images input
+ this.uploadImagesInput = $el("input", {
+ type: "file",
+ multiple: false,
+ style: inputStyle,
+ accept: "image/*",
+ });
+
+ this.uploadImagesInput.addEventListener("change", async (e) => {
+ const file = e.target.files[0];
+ if (!file) {
+ this.previewImage.src = "";
+ this.previewImage.style.display = "none";
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ const imgData = e.target.result;
+ this.previewImage.src = imgData;
+ this.previewImage.style.display = "block";
+ this.selectedFile = null;
+ // Once user uploads an image, we uncheck all radio buttons
+ this.radioButtons.forEach((ele) => {
+ ele.checked = false;
+ ele.parentElement.classList.remove("checked");
+ });
+
+ // Add the opacity style toggle here to indicate that they only need
+ // to upload one image or choose one from the outputs.
+ this.outputsSection.style.opacity = 0.35;
+ this.uploadImagesInput.style.opacity = 1;
+ };
+ reader.readAsDataURL(file);
+ });
+
+ // preview image
+ this.previewImage = $el("img", {
+ src: "",
+ style: {
+ width: "100%",
+ maxHeight: "100px",
+ objectFit: "contain",
+ display: "none",
+ marginTop: "10px",
+ },
+ });
+
+ this.keyInput = $el("input", {
+ type: "password",
+ placeholder: "Copy & paste your API key",
+ style: inputStyle,
+ });
+ this.TitleInput = $el("input", {
+ type: "text",
+ placeholder: "Title (Required)",
+ style: inputStyle,
+ maxLength: "70",
+ oninput: () => {
+ const titleNum = this.TitleInput.value.length;
+ titleNumDom.textContent = `${titleNum}/70`;
+ },
+ });
+ this.SubTitleInput = $el("input", {
+ type: "text",
+ placeholder: "Subtitle (Optional)",
+ style: inputStyle,
+ maxLength: "70",
+ oninput: () => {
+ const titleNum = this.SubTitleInput.value.length;
+ subTitleNumDom.textContent = `${titleNum}/70`;
+ },
+ });
+ this.descriptionInput = $el("textarea", {
+ placeholder: "Content (Optional)",
+ style: {
+ ...textAreaStyle,
+ minHeight: "100px",
+ },
+ });
+
+ // Header Section
+ const headerSection = $el("h3", {
+ textContent: "Share your workflow to Copus",
+ size: 3,
+ color: "white",
+ style: {
+ "text-align": "center",
+ color: "white",
+ margin: "0 0 10px 0",
+ },
+ });
+ this.getAPIKeyLink = $el(
+ "a",
+ {
+ style: {
+ ...hyperLinkStyle,
+ color: "#59E8C6",
+ },
+ href: `${DEFAULT_HOMEPAGE_URL}?fromPage=comfyUI`,
+ target: "_blank",
+ },
+ ["👉 Get your API key here"]
+ );
+ const linkSection = $el(
+ "div",
+ {
+ style: {
+ marginTop: "10px",
+ display: "flex",
+ flexDirection: "column",
+ },
+ },
+ [
+ // this.communityLink,
+ this.getAPIKeyLink,
+ ]
+ );
+
+ // Account Section
+ const accountSection = $el("div", { style: sectionStyle }, [
+ $el("label", { style: labelStyle }, ["1️⃣ Copus API Key"]),
+ this.keyInput,
+ ]);
+
+ // Output Upload Section
+ const outputUploadSection = $el("div", { style: sectionStyle }, [
+ $el(
+ "label",
+ {
+ style: {
+ ...labelStyle,
+ margin: "10px 0 0 0",
+ },
+ },
+ ["2️⃣ Image/Thumbnail (Required)"]
+ ),
+ this.previewImage,
+ this.uploadImagesInput,
+ ]);
+
+ // Outputs Section
+ this.outputsSection = $el(
+ "div",
+ {
+ id: "selectOutputs",
+ },
+ []
+ );
+
+ const titleNumDom = $el(
+ "label",
+ {
+ style: {
+ fontSize: "12px",
+ position: "absolute",
+ right: "10px",
+ bottom: "-10px",
+ color: "#999",
+ },
+ },
+ ["0/70"]
+ );
+ const subTitleNumDom = $el(
+ "label",
+ {
+ style: {
+ fontSize: "12px",
+ position: "absolute",
+ right: "10px",
+ bottom: "-10px",
+ color: "#999",
+ },
+ },
+ ["0/70"]
+ );
+ const descriptionNumDom = $el(
+ "label",
+ {
+ style: {
+ fontSize: "12px",
+ position: "absolute",
+ right: "10px",
+ bottom: "-10px",
+ color: "#999",
+ },
+ },
+ ["0/70"]
+ );
+ // Additional Inputs Section
+ const additionalInputsSection = $el(
+ "div",
+ { style: { ...sectionStyle, } },
+ [
+ $el("label", { style: labelStyle }, ["3️⃣ Title "]),
+ this.TitleInput,
+ titleNumDom,
+ ]
+ );
+ const SubtitleSection = $el("div", { style: sectionStyle }, [
+ $el("label", { style: labelStyle }, ["4️⃣ Subtitle "]),
+ this.SubTitleInput,
+ subTitleNumDom,
+ ]);
+ const DescriptionSection = $el("div", { style: sectionStyle }, [
+ $el("label", { style: labelStyle }, ["5️⃣ Description "]),
+ this.descriptionInput,
+ // descriptionNumDom,
+ ]);
+ // switch between outputs section and additional inputs section
+ this.radioButtons = [];
+
+ this.radioButtonsCheck = $el("input", {
+ type: "radio",
+ name: "output_type",
+ value: "0",
+ id: "blockchain1",
+ checked: true,
+ });
+ this.radioButtonsCheckOff = $el("input", {
+ type: "radio",
+ name: "output_type",
+ value: "1",
+ id: "blockchain",
+ });
+
+ const blockChainSection = $el("div", { style: sectionStyle }, [
+ $el("label", { style: labelStyle }, ["6️⃣ Store on blockchain "]),
+ $el(
+ "label",
+ {
+ style: {
+ marginTop: "10px",
+ display: "flex",
+ alignItems: "center",
+ cursor: "pointer",
+ },
+ },
+ [
+ this.radioButtonsCheck,
+ $el("span", { style: { marginLeft: "5px" } }, ["ON"]),
+ ]
+ ),
+ $el(
+ "label",
+ { style: { display: "flex", alignItems: "center", cursor: "pointer" } },
+ [
+ this.radioButtonsCheckOff,
+ $el("span", { style: { marginLeft: "5px" } }, ["OFF"]),
+ ]
+ ),
+ $el(
+ "p",
+ { style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } },
+ ["Secure ownership with a permanent & decentralized storage"]
+ ),
+ ]);
+ // Message Section
+ this.message = $el(
+ "div",
+ {
+ style: {
+ color: "#ff3d00",
+ textAlign: "center",
+ padding: "10px",
+ fontSize: "20px",
+ },
+ },
+ []
+ );
+
+ this.shareButton = $el("button", {
+ type: "submit",
+ textContent: "Share",
+ style: buttonStyle,
+ onclick: () => {
+ this.handleShareButtonClick();
+ },
+ });
+
+ // Share and Close Buttons
+ const buttonsSection = $el(
+ "div",
+ {
+ style: {
+ textAlign: "right",
+ marginTop: "20px",
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ },
+ [
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ style: {
+ ...buttonStyle,
+ backgroundColor: undefined,
+ },
+ onclick: () => {
+ this.close();
+ },
+ }),
+ this.shareButton,
+ ]
+ );
+
+ // Composing the full layout
+ const layout = [
+ headerSection,
+ linkSection,
+ accountSection,
+ outputUploadSection,
+ this.outputsSection,
+ additionalInputsSection,
+ SubtitleSection,
+ DescriptionSection,
+ // contestSection,
+ blockChainSection,
+ this.message,
+ buttonsSection,
+ ];
+
+ return layout;
+ }
+ /**
+ * api
+ * @param {url} path
+ * @param {params} options
+ * @param {statusText} statusText
+ * @returns
+ */
+ async fetchApi(path, options, statusText) {
+ if (statusText) {
+ this.message.textContent = statusText;
+ }
+ const fullPath = new URL(API_ENDPOINT + path);
+ const response = await fetch(fullPath, options);
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ if (statusText) {
+ this.message.textContent = "";
+ }
+ const data = await response.json();
+ return {
+ ok: response.ok,
+ statusText: response.statusText,
+ status: response.status,
+ data,
+ };
+ }
+ /**
+ * @param {file} uploadFile
+ */
+ async uploadThumbnail(uploadFile, type) {
+ const form = new FormData();
+ form.append("file", uploadFile);
+ form.append("apiToken", this.keyInput.value);
+ try {
+ const res = await this.fetchApi(
+ `/client/common/opus/uploadImage`,
+ {
+ method: "POST",
+ body: form,
+ },
+ "Uploading thumbnail..."
+ );
+ if (res.status && res.data.status && res.data) {
+ const { data } = res.data;
+ if (type) {
+ this.allFilesImages.push({
+ url: data,
+ });
+ }
+ this.uploadedImages.push({
+ url: data,
+ });
+ } else {
+ throw new Error("make sure your API key is correct and try again later");
+ }
+ } 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 = "";
+ try {
+ this.shareButton.disabled = true;
+ this.shareButton.textContent = "Sharing...";
+ await this.share();
+ } catch (e) {
+ alert(e.message);
+ }
+ this.shareButton.disabled = false;
+ this.shareButton.textContent = "Share";
+ }
+ /**
+ * share
+ * @param {string} title
+ * @param {string} subtitle
+ * @param {string} content
+ * @param {boolean} storeOnChain
+ * @param {string} coverUrl
+ * @param {string[]} imageUrls
+ * @param {string} apiToken
+ */
+ async share() {
+ const prompt = await app.graphToPrompt();
+ const workflowJSON = prompt["workflow"];
+ const form_values = {
+ title: this.TitleInput.value,
+ subTitle: this.SubTitleInput.value,
+ content: this.descriptionInput.value,
+ storeOnChain: this.radioButtonsCheck.checked ? true : false,
+ };
+
+ if (!this.keyInput.value) {
+ throw new Error("API key is required");
+ }
+
+ if (!this.uploadImagesInput.files[0] && !this.selectedFile) {
+ throw new Error("Thumbnail is required");
+ }
+
+ if (!form_values.title) {
+ throw new Error("Title is required");
+ }
+
+ if (!this.uploadedImages.length) {
+ if (this.selectedFile) {
+ await this.uploadThumbnail(this.selectedFile);
+ } else {
+ for (const file of this.uploadImagesInput.files) {
+ try {
+ await this.uploadThumbnail(file);
+ } catch (e) {
+ this.uploadedImages = [];
+ throw new Error(e.message);
+ }
+ }
+
+ if (this.uploadImagesInput.files.length === 0) {
+ throw new Error("No thumbnail uploaded");
+ }
+ }
+ }
+ if (this.allFiles.length > 0) {
+ for (const file of this.allFiles) {
+ try {
+ await this.uploadThumbnail(file, true);
+ } catch (e) {
+ this.allFilesImages = [];
+ throw new Error(e.message);
+ }
+ }
+ }
+ try {
+ const res = await this.fetchApi(
+ "/client/common/opus/shareFromComfyUI",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ workflowJson: workflowJSON,
+ apiToken: this.keyInput.value,
+ coverUrl: this.uploadedImages[0].url,
+ imageUrls: this.allFilesImages.map((image) => image.url),
+ ...form_values,
+ }),
+ },
+ "Uploading workflow..."
+ );
+
+ if (res.status && res.data.status && res.data) {
+ localStorage.setItem("copus_token",this.keyInput.value);
+ const { data } = res.data;
+ if (data) {
+ const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
+ this.message.innerHTML = `Workflow has been shared successfully. Click here to view it.`;
+ this.previewImage.src = "";
+ this.previewImage.style.display = "none";
+ this.uploadedImages = [];
+ this.allFilesImages = [];
+ this.allFiles = [];
+ this.TitleInput.value = "";
+ this.SubTitleInput.value = "";
+ this.descriptionInput.value = "";
+ 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({ potential_outputs, potential_output_nodes } = {}) {
+ // Sort `potential_output_nodes` by node ID to make the order always
+ // consistent, but we should also keep `potential_outputs` in the same
+ // order as `potential_output_nodes`.
+ const potential_output_to_order = {};
+ potential_output_nodes.forEach((node, index) => {
+ if (node.id in potential_output_to_order) {
+ potential_output_to_order[node.id][1].push(potential_outputs[index]);
+ } else {
+ potential_output_to_order[node.id] = [node, [potential_outputs[index]]];
+ }
+ });
+ // Sort the object `potential_output_to_order` by key (node ID)
+ const sorted_potential_output_to_order = Object.fromEntries(
+ Object.entries(potential_output_to_order).sort(
+ (a, b) => a[0].id - b[0].id
+ )
+ );
+ const sorted_potential_outputs = [];
+ const sorted_potential_output_nodes = [];
+ for (const [key, value] of Object.entries(
+ sorted_potential_output_to_order
+ )) {
+ sorted_potential_output_nodes.push(value[0]);
+ sorted_potential_outputs.push(...value[1]);
+ }
+ potential_output_nodes = sorted_potential_output_nodes;
+ potential_outputs = sorted_potential_outputs;
+ const apiToken = localStorage.getItem("copus_token");
+ this.message.innerHTML = "";
+ this.message.textContent = "";
+ this.element.style.display = "block";
+ this.previewImage.src = "";
+ this.previewImage.style.display = "none";
+ this.keyInput.value = apiToken!=null?apiToken:"";
+ this.uploadedImages = [];
+ this.allFilesImages = [];
+ this.allFiles = [];
+ // 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 = potential_output_nodes.findIndex(
+ (node) => node.id === this.selectedNodeId
+ );
+ if (index >= 0) {
+ this.selectedOutputIndex = index;
+ }
+ }
+
+ this.radioButtons = [];
+ const new_radio_buttons = $el(
+ "div",
+ {
+ id: "selectOutput-Options",
+ style: {
+ "overflow-y": "scroll",
+ "max-height": "200px",
+ 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)",
+ },
+ },
+ potential_outputs.map((output, index) => {
+ const { node_id } = output;
+ const radio_button = $el(
+ "input",
+ {
+ type: "radio",
+ name: "selectOutputImages",
+ value: index,
+ required: index === 0,
+ },
+ []
+ );
+ let radio_button_img;
+ let filename;
+ if (output.type === "image" || output.type === "temp") {
+ radio_button_img = $el(
+ "img",
+ {
+ src: `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`,
+ style: {
+ width: "100px",
+ height: "100px",
+ objectFit: "cover",
+ borderRadius: "5px",
+ },
+ },
+ []
+ );
+ filename = output.image.filename;
+ } else if (output.type === "output") {
+ radio_button_img = $el(
+ "img",
+ {
+ src: output.output.value,
+ style: {
+ width: "auto",
+ height: "100px",
+ objectFit: "cover",
+ borderRadius: "5px",
+ },
+ },
+ []
+ );
+ filename = output.filename;
+ } else {
+ // unsupported output type
+ // this should never happen
+ radio_button_img = $el(
+ "img",
+ {
+ src: "",
+ style: { width: "auto", height: "100px" },
+ },
+ []
+ );
+ }
+ const radio_button_text = $el(
+ "span",
+ {
+ style: {
+ color: "gray",
+ display: "block",
+ fontSize: "12px",
+ overflowX: "hidden",
+ textOverflow: "ellipsis",
+ textWrap: "nowrap",
+ maxWidth: "100px",
+ },
+ },
+ [output.title]
+ );
+ const node_id_chip = $el(
+ "span",
+ {
+ style: {
+ color: "#FBFBFD",
+ display: "block",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ fontSize: "12px",
+ overflowX: "hidden",
+ padding: "2px 3px",
+ textOverflow: "ellipsis",
+ textWrap: "nowrap",
+ maxWidth: "100px",
+ position: "absolute",
+ top: "3px",
+ left: "3px",
+ borderRadius: "3px",
+ },
+ },
+ [`Node: ${node_id}`]
+ );
+ radio_button.style.color = "var(--fg-color)";
+ radio_button.checked = this.selectedOutputIndex === index;
+
+ radio_button.onchange = async () => {
+ this.selectedOutputIndex = parseInt(radio_button.value);
+
+ // Remove the "checked" class from all radio buttons
+ this.radioButtons.forEach((ele) => {
+ ele.parentElement.classList.remove("checked");
+ });
+ radio_button.parentElement.classList.add("checked");
+
+ this.fetchImageBlob(radio_button_img.src).then((blob) => {
+ const file = new File([blob], filename, {
+ type: blob.type,
+ });
+ this.previewImage.src = radio_button_img.src;
+ this.previewImage.style.display = "block";
+ this.selectedFile = file;
+ });
+
+ // Add the opacity style toggle here to indicate that they only need
+ // to upload one image or choose one from the outputs.
+ this.outputsSection.style.opacity = 1;
+ this.uploadImagesInput.style.opacity = 0.35;
+ };
+
+ if (radio_button.checked) {
+ this.fetchImageBlob(radio_button_img.src).then((blob) => {
+ const file = new File([blob], filename, {
+ type: blob.type,
+ });
+ this.previewImage.src = radio_button_img.src;
+ this.previewImage.style.display = "block";
+ this.selectedFile = file;
+ });
+ // Add the opacity style toggle here to indicate that they only need
+ // to upload one image or choose one from the outputs.
+ this.outputsSection.style.opacity = 1;
+ this.uploadImagesInput.style.opacity = 0.35;
+ }
+ this.radioButtons.push(radio_button);
+ let src = "";
+ if (output.type === "image" || output.type === "temp") {
+ filename = output.image.filename;
+ src = `/view?filename=${output.image.filename}&subfolder=${output.image.subfolder}&type=${output.image.type}`;
+ } else if (output.type === "output") {
+ src = output.output.value;
+ filename = output.filename;
+ }
+ if (src) {
+ this.fetchImageBlob(src).then((blob) => {
+ const file = new File([blob], filename, {
+ type: blob.type,
+ });
+ this.allFiles.push(file);
+ });
+ }
+ return $el(
+ `label.output_label${radio_button.checked ? ".checked" : ""}`,
+ {
+ style: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: "10px",
+ cursor: "pointer",
+ position: "relative",
+ },
+ },
+ [radio_button_img, radio_button_text, radio_button, node_id_chip]
+ );
+ })
+ );
+
+ const header = $el(
+ "p",
+ {
+ textContent:
+ this.radioButtons.length === 0
+ ? "Queue Prompt to see the outputs"
+ : "Or choose one from the outputs (scroll to see all)",
+ size: 2,
+ color: "white",
+ style: {
+ color: "white",
+ margin: "0 0 5px 0",
+ fontSize: "12px",
+ },
+ },
+ []
+ );
+ this.outputsSection.innerHTML = "";
+ this.outputsSection.appendChild(header);
+ this.outputsSection.appendChild(new_radio_buttons);
+ }
+}