From d13a6e4b2a308a6999a2106a75d044478ef242d9 Mon Sep 17 00:00:00 2001 From: pingren <5123601+Pingren@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:56:57 +0800 Subject: [PATCH] feat(WIP): share workflow --- js/comfyui-share-openart.js | 440 +++++++++++++++++++++++++----------- 1 file changed, 311 insertions(+), 129 deletions(-) diff --git a/js/comfyui-share-openart.js b/js/comfyui-share-openart.js index 68401ad5..0aac2000 100644 --- a/js/comfyui-share-openart.js +++ b/js/comfyui-share-openart.js @@ -1,10 +1,13 @@ import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js" +import { api } from "../../scripts/api.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; const LOCAL_STORAGE_KEY = "openart_comfy_workflow_key"; const DEFAULT_HOMEPAGE_URL = "https://openart.ai/workflows/dev?developer=true"; +// const API_ENDPOINT = "https://openart.ai/api"; +const API_ENDPOINT = "http://localhost:8080/api"; + const style = ` .openart-share-dialog a { color: #f8f8f8; @@ -15,144 +18,323 @@ const style = ` `; export class OpenArtShareDialog extends ComfyDialog { - static instance = null; + static instance = null; - constructor() { - super(); - $el("style", { - textContent: style, - parent: document.head, - }); - this.element = $el("div.comfy-modal.openart-share-dialog", { - parent: document.body, style: { - 'overflow-y': "auto", - } - }, - [$el("div.comfy-modal-content", - {}, - [...this.createButtons()]), - ]); - this.selectedOutputIndex = 0; - } - readKeyFromLocalStorage() { - return localStorage.getItem(LOCAL_STORAGE_KEY) || ''; - } - saveKeyToLocalStorage(value) { - localStorage.setItem(LOCAL_STORAGE_KEY, value); - } - createButtons() { - const sectionStyle = { - marginBottom: '10px', - padding: '15px', - borderRadius: '8px', - boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)' - }; + constructor() { + super(); + $el("style", { + textContent: style, + parent: document.head, + }); + this.element = $el( + "div.comfy-modal.openart-share-dialog", + { + parent: document.body, + style: { + "overflow-y": "auto", + }, + }, + [$el("div.comfy-modal-content", {}, [...this.createButtons()])] + ); + this.selectedOutputIndex = 0; + this.uploadedImages = []; + } + readKeyFromLocalStorage() { + return localStorage.getItem(LOCAL_STORAGE_KEY) || ""; + } + saveKeyToLocalStorage(value) { + localStorage.setItem(LOCAL_STORAGE_KEY, value); + } + createButtons() { + const sectionStyle = { + marginBottom: "10px", + padding: "15px", + borderRadius: "8px", + boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)", + }; - const inputStyle = { - display: 'block', - minWidth: '500px', - width: '100%', - padding: '10px', - margin: '10px 0', - borderRadius: '4px', - border: '1px solid #ddd', - boxSizing: 'border-box' - }; + const inputStyle = { + display: "block", + minWidth: "500px", + width: "100%", + padding: "10px", + margin: "10px 0", + borderRadius: "4px", + border: "1px solid #ddd", + boxSizing: "border-box", + }; - const hyperLinkStyle = { - display: 'block', - marginBottom: '15px', - fontWeight: 'bold', - fontSize: '20px', - }; + const hyperLinkStyle = { + display: "block", + marginBottom: "15px", + fontWeight: "bold", + fontSize: "20px", + }; - const labelStyle = { - color: "#f8f8f8", - display: 'block', - margin: '10px 0', - fontWeight: 'bold', - textDecoration: 'none', - }; + const labelStyle = { + color: "#f8f8f8", + display: "block", + margin: "10px 0", + fontWeight: "bold", + textDecoration: "none", + }; - const buttonStyle = { - padding: '10px 80px', - margin: '10px 5px', - borderRadius: '4px', - border: 'none', - cursor: 'pointer', - color: '#fff', - backgroundColor: '#007bff' - }; + 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 }) - - this.uploadImagesInput.addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) { - return; - } - const reader = new FileReader(); - reader.onload = async (e) => { - const imgData = e.target.result; - this.previewImage.src = imgData; - }; - reader.readAsDataURL(file); - }); + // upload images input + this.uploadImagesInput = $el("input", { + type: "file", + multiple: false, + style: inputStyle, + }); - // preview image - this.previewImage = $el("img", { src: "", style: { maxWidth: '100%', maxHeight: '100px' } }); + this.uploadImagesInput.addEventListener("change", async (e) => { + const file = e.target.files[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = async (e) => { + const imgData = e.target.result; + this.previewImage.src = imgData; + }; + reader.readAsDataURL(file); + }); - this.keyInput = $el("input", { type: 'text', placeholder: "Copy & paste your API key", style: inputStyle }) - // Account Section - const AccountSection = $el("div", { style: sectionStyle }, [ - $el("a", { style: hyperLinkStyle, href: DEFAULT_HOMEPAGE_URL }, ["Check out 1000+ workflows others have uploaded."]), - $el("a", { style: hyperLinkStyle, href: DEFAULT_HOMEPAGE_URL }, ["You can get API key at here."]), - $el("label", { style: labelStyle }, ["OpenArt API Key"]), - this.keyInput, - ]); + // preview image + this.previewImage = $el("img", { + src: "", + style: { maxWidth: "100%", maxHeight: "100px" }, + }); - // Additional Inputs Section - const additionalInputsSection = $el("div", { style: sectionStyle }, [ - $el("label", { style: labelStyle }, ["Image/Thumbnail (Required)"]), - this.uploadImagesInput, - this.previewImage, - $el("label", { style: labelStyle }, ["Workflow Information"]), - $el("input", { type: "text", placeholder: "Name (required)", style: inputStyle }), - $el("textarea", { placeholder: "Description (optional)", style: { - ...inputStyle, - minHeight: '100px', - } }), - ]); + this.keyInput = $el("input", { + type: "text", + placeholder: "Copy & paste your API key", + style: inputStyle, + }); + this.NameInput = $el("input", { + type: "text", + placeholder: "Name (required)", + style: inputStyle, + }); + this.descriptionInput = $el("textarea", { + placeholder: "Description (optional)", + style: { + ...inputStyle, + minHeight: "100px", + }, + }); - // 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(); } }), - $el("button", { type: "submit", textContent: "Share", style: buttonStyle, onclick: () => { this.share(); } }), - ]); + // Account Section + const AccountSection = $el("div", { style: sectionStyle }, [ + $el("a", { style: hyperLinkStyle, href: DEFAULT_HOMEPAGE_URL }, [ + "Check out 1000+ workflows others have uploaded.", + ]), + $el("a", { style: hyperLinkStyle, href: DEFAULT_HOMEPAGE_URL }, [ + "You can get API key at here.", + ]), + $el("label", { style: labelStyle }, ["OpenArt API Key"]), + this.keyInput, + ]); - // Final Message Section - const finalMessage = $el("div", { style: { color: "white", textAlign: "center", padding: "10px" } }, []); - // Composing the full layout - const layout = [ - AccountSection, - additionalInputsSection, - buttonsSection, - finalMessage, - ]; + // Additional Inputs Section + const additionalInputsSection = $el("div", { style: sectionStyle }, [ + $el("label", { style: labelStyle }, ["Image/Thumbnail (Required)"]), + this.uploadImagesInput, + this.previewImage, + $el("label", { style: labelStyle }, ["Workflow Information"]), + this.NameInput, + this.descriptionInput, + ]); - return layout; + 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, + ] + ); + + // Message Section + this.message = $el( + "div", + { + style: { + color: "#ff3d00", + textAlign: "center", + padding: "10px", + fontSize: "20px", + }, + }, + [] + ); + // Composing the full layout + const layout = [ + AccountSection, + additionalInputsSection, + this.message, + buttonsSection, + ]; + + return layout; + } + async fetchApi(path, options, statusText) { + if (statusText) { + this.message.textContent = statusText; } - async share() { - this.saveKeyToLocalStorage(this.keyInput.value); - } - show({ potential_outputs, potential_output_nodes }) { - this.element.style.display = "block"; - // read key from local storage and set it to the input - const key = this.readKeyFromLocalStorage(); - this.keyInput.value = key; - } -} \ No newline at end of file + const addSearchParams = (url, params = {}) => + new URL( + `${url.origin}${url.pathname}?${new URLSearchParams([ + ...Array.from(url.searchParams.entries()), + ...Object.entries(params), + ])}` + ); + + const fullPath = addSearchParams(new URL(API_ENDPOINT + path), { + workflow_api_key: this.keyInput.value, + }); + + const response = await fetch(fullPath, options); + + if (statusText) { + this.message.textContent = ""; + } + + return await response.json(); + } + async uploadThumbnail(uploadFile) { + const form = new FormData(); + form.append("file", uploadFile); + try { + const res = await this.fetchApi( + `/workflows/upload_thumbnail`, + { + method: "POST", + body: form, + }, + "Uploading thumbnail..." + ); + if (res.status === 200 && res.data) { + const { image_url, width, height } = res.data; + this.uploadedImages.push({ + url: image_url, + width, + height, + }); + } + } 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() { + 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"; + this.message.textContent = ""; + this.uploadedImages = []; + } + async share() { + this.uploadedImages = []; + const prompt = await app.graphToPrompt(); + const workflowJSON = prompt["workflow"]; + const form_values = { + name: this.NameInput.value, + description: this.descriptionInput.value, + }; + + if (!this.keyInput.value) { + throw new Error("API key is required"); + } + + if (!this.uploadImagesInput.files[0]) { + throw new Error("Thumbnail is required"); + } + + if (!form_values.name) { + throw new Error("Name is required"); + } + + for (const file of this.uploadImagesInput.files) { + await this.uploadThumbnail(file); + } + + if (this.uploadImagesInput.files.length === 0) { + throw new Error("No thumbnail uploaded"); + } + + try { + const response = await this.fetchApi( + "/workflows/publish", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + workflow_json: workflowJSON, + upload_images: this.uploadedImages, + form_values, + }), + }, + "Uploading workflow..." + ); + + if (response.status === 200) { + console.log(response.data); + this.saveKeyToLocalStorage(this.keyInput.value); + } + } catch (e) { + throw new Error("Error sharing workflow: " + e.message); + } + } + show({ potential_outputs, potential_output_nodes }) { + this.element.style.display = "block"; + // read key from local storage and set it to the input + const key = this.readKeyFromLocalStorage(); + this.keyInput.value = key; + } +}