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); + } +}