diff --git a/js/button/actions.js b/js/button/actions.js index 9e9e777..a3ad7f9 100644 --- a/js/button/actions.js +++ b/js/button/actions.js @@ -7,11 +7,9 @@ import { createRun } from "../client.js" import { - getData, createMetaNode, getApiToken, validatePrompt, - getWorkflowId, compareWorkflows, isWorkflowUpToDate } from "../utils.js" @@ -19,7 +17,6 @@ import { app, } from '../comfy/comfy.js'; import { - configDialog, infoDialog, } from '../comfy/ui.js'; import { @@ -28,12 +25,12 @@ import { setMessage, } from './ui.js'; import { logger } from '../logger.js'; +import { authDialog } from '../node/auth.ui.js'; export async function onGeneration() { logger.newLog(); try { - const { endpoint } = getData(); setButtonDefault() // check auth @@ -42,7 +39,8 @@ export async function onGeneration() { if(!doesApiTokenExist) { // Request auth - configDialog.show(); + setButtonDefault() + return authDialog.show(); } // check if ComfyCloud meta node exists @@ -79,11 +77,9 @@ export async function onGeneration() { await pollSyncDependencies(s.taskId) } - if(s?.nodesToUpload) { - setMessage("Building environment..."); - } + setMessage("Updating workflow..."); - await uploadLocalWorkflow() + await uploadLocalWorkflow(s) } // @todo patch workflow inputs @@ -92,11 +88,10 @@ export async function onGeneration() { // and workflow api is synced to the cloud // create run - const workflow_id = getWorkflowId() await createRun() infoDialog.showMessage( "Item queued!", - `View your generation results at this URL: ${endpoint}/workflows/${workflow_id}`, + "You can view your generation results by clicking the 'Menu' button in your Comfy Cloud custom node." ) } catch (e) { // handle error diff --git a/js/client.js b/js/client.js index 2629f13..61b88f7 100644 --- a/js/client.js +++ b/js/client.js @@ -1,10 +1,8 @@ +import { setData, getData } from './store.js'; import { - getData, getUser, getWorkflowId, - getVersion, setWorkflowId, - setVersion, getWorkflowName, getCustomNodesList, isCustomNode, @@ -14,18 +12,69 @@ import { app } from './comfy/comfy.js'; import retry from "https://esm.sh/gh/vercel/async-retry"; import { endpoint } from './constants.js'; +export const loginUser = async ({ email, password }) => { + const apiRoute = endpoint + "/auth/login"; + const body = { + email: email, + password: password, + } + const res = await fetch(apiRoute, { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }) + const data = await res.json() + + // @todo - save api token + if(res.status == 200 && data) { + setData({ + apiKey: data.token, + user: data.user + }) + } + + return { ...data, status: res.status }; +} + +export const registerUser = async ({ email, password }) => { + const apiRoute = endpoint + "/auth/register"; + const body = { + email: email, + password: password, + } + const res = await fetch(apiRoute, { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }) + + const data = await res.json() + + if(res.status == 200 && data) { + setData({ + apiKey: data.token, + user: data.user + }) + } + + return { ...data, status: res.status }; +} + /** * Calls endpoints from ComfyCloud API * * */ - const fetchRetry = async (url, options = {}, retries) => { return await retry( async bail => { logger.info("fetchRetry: sending request...") const res = await fetch(url, options); - if (res.status !== 200) { + if (res.status !== 200 && res.status !== 201) { bail(new Error("Server error")); return; } @@ -44,21 +93,16 @@ const fetchRetry = async (url, options = {}, retries) => { export const getCloudWorkflow = async () => { logger.info("Retrieving existing workflow from cloud") try { - const { endpoint, apiKey } = getData(); + const { apiKey } = getData(); const workflow_id = getWorkflowId(); - const body = { - version: parseInt(getVersion()), - } - const existing_workflow = await fetchRetry(endpoint + "/api/workflow/" + workflow_id, { - method: "POST", - body: JSON.stringify(body), + const existing_workflow = await fetchRetry(endpoint + "/workflow/" + workflow_id, { + method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }, 2) - logger.info("Successfully retrieved workflow") return existing_workflow; @@ -69,22 +113,48 @@ export const getCloudWorkflow = async () => { } } + +export const getWorkflowRunOutput = async (run_id) => { + logger.info("Retrieving existing workflow from cloud") + try { + const { apiKey } = getData(); + const workflow_id = getWorkflowId(); + + const output = await fetchRetry(endpoint + "/workflow/" + workflow_id + "/runs/" + run_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + apiKey, + }, + }, 2) + logger.info("Successfully retrieved workflow output") + + return output; + } catch(e) { + logger.error("Failed to retrieve workflow output", e) + + throw new Error("Failed to retrieve workflow output") + } +} + /** * Sends data to createRun api endpoint */ export const createRun = async () => { logger.info("Creating run workflow request") try { - const { endpoint, apiKey } = getData(); + const { apiKey } = getData(); const user = await getUser(); + const workflow_id = getWorkflowId() - const apiRoute = endpoint + "/api/run"; + const apiRoute = endpoint + "/workflow/" + workflow_id + "/runs"; const body = { - workflow_id: getWorkflowId(), - version: getVersion(), + workflow_id, + //version: getVersion(), user_id: user?.id, inputs: {}, } + await fetch(apiRoute, { method: "POST", body: JSON.stringify(body), @@ -106,7 +176,9 @@ export const createRun = async () => { export const createEmptyWorkflow = async () => { logger.info("Uploading local workflow to cloud") try { - const { endpoint, apiKey } = getData(); + const { apiKey } = getData(); + const apiRoute = endpoint + "/workflow"; + /* const apiRoute = endpoint + "/api/workflow"; const body = { @@ -120,7 +192,11 @@ export const createEmptyWorkflow = async () => { file_custom_nodes: [], }, }; + */ //let data = { status: 200 } + const body = { + name: getWorkflowName(), + } let data = await fetchRetry(apiRoute, { method: "POST", body: JSON.stringify(body), @@ -130,8 +206,8 @@ export const createEmptyWorkflow = async () => { }, }, 2); - setWorkflowId(data.workflow_id) - setVersion(data.version) + setWorkflowId(data.id) + //setVersion(data.version) logger.info("Successfully uploaded local workflow to cloud") } catch (e) { @@ -148,27 +224,23 @@ export const createEmptyWorkflow = async () => { /** * Sends local workflow to the cloud to be stored */ -export const uploadLocalWorkflow = async () => { +export const uploadLocalWorkflow = async (dependencies) => { logger.info("Uploading local workflow to cloud") try { - const { endpoint, apiKey } = getData(); - const apiRoute = endpoint + "/api/workflow"; + const { apiKey } = getData(); + const workflow_id = getWorkflowId() + const apiRoute = endpoint + "/workflow/" + workflow_id; const prompt = await app.graphToPrompt(); const body = { - workflow_name: getWorkflowName(), - workflow_id: getWorkflowId(), + name: getWorkflowName(), workflow: prompt.workflow, workflow_api: prompt.output, - snapshot: { - comfyui: "", - git_custom_nodes: {}, - file_custom_nodes: [], - }, + dependencies: dependencies, }; //let data = { status: 200 } let data = await fetchRetry(apiRoute, { - method: "POST", + method: "PUT", body: JSON.stringify(body), headers: { "Content-Type": "application/json", @@ -176,8 +248,8 @@ export const uploadLocalWorkflow = async () => { }, }, 2); - setWorkflowId(data.workflow_id) - setVersion(data.version) + setWorkflowId(data.id) + //setVersion(data.version) logger.info("Successfully uploaded local workflow to cloud") } catch (e) { @@ -196,7 +268,7 @@ export const syncDependencies = async (diff) => { try { // update with new workflow const workflow_id = getWorkflowId() - const { endpoint } = getData(); + const { apiKey } = getData(); if(!diff) { return {} @@ -277,6 +349,10 @@ export const syncDependencies = async (diff) => { const res = await fetch("/comfy-cloud/upload", { method: "POST", body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + apiKey, + }, }) if(res.status == 200) { diff --git a/js/comfy/ui.js b/js/comfy/ui.js index 0a804ad..4905374 100644 --- a/js/comfy/ui.js +++ b/js/comfy/ui.js @@ -1,5 +1,5 @@ import { ComfyDialog, $el } from './comfy.js'; -import { getData, saveData } from '../utils.js'; +import { getData, setData } from '../store.js'; export class InfoDialog extends ComfyDialog { constructor() { @@ -33,7 +33,7 @@ export class InfoDialog extends ComfyDialog { this.textElement.replaceChildren(html); } this.element.style.display = "flex"; - this.element.style.zIndex = 1001; + this.element.style.zIndex = 1002; } showMessage(title, message) { @@ -281,7 +281,7 @@ export class ConfigDialog extends ComfyDialog { //localStorage.setItem("comfy_cloud_env", deployOption); const endpoint = this.endpoint - saveData({ + setData({ endpoint, apiKey: api_key, displayName, diff --git a/js/constants.js b/js/constants.js index a092894..e8e79ef 100644 --- a/js/constants.js +++ b/js/constants.js @@ -1,2 +1,3 @@ -export const endpoint = "https://comfycloud.vercel.app" -//export const endpoint = "http://localhost:3000" +//export const endpoint = "https://comfycloud.vercel.app" +//export const endpoint = "http://localhost:4000" +export const endpoint = "https://comfyui-cloud-a0cf78bd8c3d.herokuapp.com" diff --git a/js/logger.js b/js/logger.js index 20826dd..02dd766 100644 --- a/js/logger.js +++ b/js/logger.js @@ -13,6 +13,7 @@ import { class Logger { constructor() { this.currentLogs = {} + this.newLog() } newLog() { diff --git a/js/node/auth.ui.js b/js/node/auth.ui.js new file mode 100644 index 0000000..a351c08 --- /dev/null +++ b/js/node/auth.ui.js @@ -0,0 +1,195 @@ +import { headerHtml, loadingIcon } from '../ui.js'; +import { loginUser, registerUser } from '../client.js'; +import { ComfyDialog, $el } from '../comfy/comfy.js'; +import { infoDialog } from '../comfy/ui.js'; + +class AuthDialog extends ComfyDialog { + container = null; + + constructor() { + super(); + this.element.classList.add("comfy-normal-modal"); + this.element.style.paddingBottom = "20px"; + + this.container = document.createElement("div"); + this.container.style.color = "white"; + + this.header = document.createElement("div"); + this.header.innerHTML = headerHtml + this.element.querySelector(".comfy-modal-content").prepend(this.container); + this.element.querySelector(".comfy-modal-content").prepend(this.header); + } + + createButtons() { + return [ + $el( + "div", + [ + $el("button", { + type: "button", + textContent: "Close", + onclick: () => this.close(), + }), + ], + ), + ]; + } + + close() { + this.element.style.display = "none"; + } + + show() { + this.showRegister(); + } + + showLogin() { + this.container.innerHTML = loginPageHtml; + const form = this.container.querySelector("#auth-form"); + form.onsubmit = async () => { + event.preventDefault(); + this.clearErrorMessage(); + + // Retrieve values from the input fields + const email = this.container.querySelector('#email').value; + const password = this.container.querySelector('#password').value; + + this.setButtonLoading(true) + + const data = await loginUser({ email, password }) + if(data.status !== 200 && data.message) { + this.setErrorMessage(data.message) + } + + this.setButtonLoading(false, "Login") + + console.log(data) + + if(data.status == 200) { + infoDialog.show(); + infoDialog.showMessage( + "Authenticated", + "You are now logged in", + ); + this.close() + } + } + const link = this.container.querySelector("#go-to-register"); + link.onclick = () => { + this.showRegister() + }; + + this.element.style.display = "flex"; + this.element.style.zIndex = 1001; + } + + showRegister() { + this.container.innerHTML = registerPageHtml; + const form = this.container.querySelector("#auth-form"); + form.onsubmit = async () => { + event.preventDefault(); + this.clearErrorMessage() + + // Retrieve values from the input fields + const email = this.container.querySelector('#email').value; + const password = this.container.querySelector('#password').value; + const confirmPassword = this.container.querySelector('#password2').value; + + if(password !== confirmPassword) { + throw new Error("Passwords don't match") + } + + this.setButtonLoading(true) + + const data = await registerUser({ email, password }) + if(data.status !== 200 && data.message) { + this.setErrorMessage(data.message) + } + this.setButtonLoading(false, "Sign up") + + if(data.status == 201) { + infoDialog.show(); + infoDialog.showMessage( + "Authenticated", + "You are now logged in", + ); + this.close() + } + } + const link = this.container.querySelector("#go-to-login"); + link.onclick = () =>{ + this.showLogin(); + } + + this.element.style.display = "flex"; + this.element.style.zIndex = 1001; + + } + + clearErrorMessage() { + this.container.querySelector('#error-message').innerHTML = ""; + } + setErrorMessage(message) { + this.container.querySelector('#error-message').innerHTML = message; + } + + setButtonLoading(isLoading, text) { + const btn = this.container.querySelector('#auth-button') + if(isLoading) { + btn.innerHTML = loadingIcon; + btn.disabled = true; + } else { + btn.innerHTML = text; + btn.disabled = false; + } + } +} +export const authDialog = new AuthDialog() + +const loginPageHtml = ` +
Status: ${rowDetails.status}
+ ${generateOutputs(rowDetails.outputs)} + +| Number | +Time | +Duration | +Status | +
|---|