diff --git a/js/chatbot/chatbot.js b/js/chatbot/chatbot.js new file mode 100644 index 0000000..7becfe7 --- /dev/null +++ b/js/chatbot/chatbot.js @@ -0,0 +1,31 @@ +import { nimbus } from "../resource/index.js"; + +export async function getBotResponse(message) { + try { + const data = await nimbus.chatbot.sendMessage({ + message: message, + origin: "ComfyUI Cloud Chat", + }); + + return data; + } catch (e) { + throw new Error(e.message); + } +} + +export function isValidWorkflow(workflow) { + return workflow && workflow.nodes && workflow.edges; +} + +export function parseWorkflowFromBot(response) { + const cleanedResponse = response.responses.bot + .replace(/^\s*```json\s*/, "") + .replace(/\s*```\s*$/, ""); + + try { + return JSON.parse(cleanedResponse); + } catch (error) { + console.error("Failed to parse JSON:", error); + return cleanedResponse; + } +} diff --git a/js/chatbot/edgeTypes.js b/js/chatbot/edgeTypes.js new file mode 100644 index 0000000..1a94310 --- /dev/null +++ b/js/chatbot/edgeTypes.js @@ -0,0 +1,406 @@ +/* eslint-disable no-unused-vars */ +export function getInputType(name) { + const nameTypeMap = { + LATENT: "LATENT", + MODEL: "MODEL", + CLIP: "CLIP", + VAE: "VAE", + CONDITIONING: "CONDITIONING", + IMAGE: "IMAGE", + MASK: "MASK", + CLIP_VISION_OUTPUT: "CLIP_VISION_OUTPUT", + positive: "CONDITIONING", + negative: "CONDITIONING", + CONTROL_NET: "CONTROL_NET", + STYLE_MODEL: "STYLE_MODEL", + CLIP_VISION: "CLIP_VISION", + GLIGEN: "GLIGEN", + latent: "LATENT", + UPSCALE_MODEL: "UPSCALE_MODEL", + output: "LATENT", + denoised_output: "LATENT", + SIGMAS: "SIGMAS", + SAMPLER: "SAMPLER", + high_sigmas: "SIGMAS", + low_sigmas: "SIGMAS", + GUIDER: "GUIDER", + NOISE: "NOISE", + PHOTOMAKER: "PHOTOMAKER", + stage_c: "LATENT", + stage_b: "LATENT", + controlnet_input: "IMAGE", + STRING: "STRING", + "*": "*", + INT: "INT", + FLOAT: "FLOAT", + BLIP_MODEL: "BLIP_MODEL", + bus: "BUS", + model: "MODEL", + clip: "CLIP", + vae: "VAE", + latent_filename: "STRING", + latent_image: "LATENT", + image_filename: "STRING", + conditioning_filename: "STRING", + NAME_STRING: "STRING", + conditioning: "CONDITIONING", + parsed_text: "STRING", + raw_text: "STRING", + NUMBER: "NUMBER", + image_a_pass: "IMAGE", + image_b_pass: "IMAGE", + filepath_text: "STRING", + filename_text: "STRING", + MASK_IMAGE: "IMAGE", + clipseg_model: "CLIPSEG_MODEL", + IMAGES_BATCH: "IMAGE", + MASKS_BATCH: "MASK", + MASK_IMAGES_BATCH: "IMAGE", + IMAGES: "IMAGE", + DICT: "DICT", + BOOLEAN: "BOOLEAN", + composited_images: "IMAGE", + ssao_images: "IMAGE", + specular_mask_images: "IMAGE", + ssdo_images: "IMAGE", + ssdo_image_masks: "IMAGE", + light_source_image_masks: "IMAGE", + aspect_number: "NUMBER", + aspect_float: "FLOAT", + is_landscape_bool: "NUMBER", + aspect_ratio_common: "STRING", + aspect_type: "STRING", + image: "IMAGE", + images: "IMAGE", + color_palettes: "LIST", + CROP_DATA: "CROP_DATA", + mask: "MASK", + shadow_map: "IMAGE", + highlight_map: "IMAGE", + width_num: "NUMBER", + height_num: "NUMBER", + width_float: "FLOAT", + height_float: "FLOAT", + width_int: "INT", + height_int: "INT", + MASKS: "MASK", + INT_PLACES: "INT", + "latent(s)": "LATENT", + tensor_w_num: "NUMBER", + tensor_h_num: "NUMBER", + masks: "MASK", + cropped_mask: "MASK", + crop_data: "CROP_DATA", + top_int: "INT", + left_int: "INT", + right_int: "INT", + bottom_int: "INT", + midas_model: "MIDAS_MODEL", + number: "NUMBER", + float: "FLOAT", + int: "INT", + SEED: "SEED", + positive_string: "STRING", + negative_string: "STRING", + seed: "SEED", + SAM_MODEL: "SAM_MODEL", + SAM_PARAMETERS: "SAM_PARAMETERS", + samples: "LATENT", + IMAGE_BOUNDS: "IMAGE_BOUNDS", + LIST: "LIST", + TEXT_A_PASS: "STRING", + TEXT_B_PASS: "STRING", + SCORE_NUMBER: "NUMBER", + COMPARISON_TEXT: "STRING", + result_text: "STRING", + replacement_count_number: "NUMBER", + replacement_count_float: "FLOAT", + replacement_count_int: "INT", + f: "B", + o: "O", + u: "O", + n: "L", + d: "E", + line_text: "STRING", + dictionary: "DICT", + MODEL_NAME_TEXT: "STRING", + image_pass: "IMAGE", + IMAGE_PASS: "IMAGE", + output_path: "STRING", + processed_count: "NUMBER", + BBOX_DETECTOR: "BBOX_DETECTOR", + SEGS: "SEGS", + cropped: "IMAGE", + cropped_refined: "IMAGE", + cropped_refined_alpha: "IMAGE", + cnet_images: "IMAGE", + segs: "SEGS", + basic_pipe: "BASIC_PIPE", + combined_mask: "MASK", + batch_masks: "MASK", + cropped_enhanced_alpha: "IMAGE", + detailer_pipe: "DETAILER_PIPE", + refiner_basic_pipe_opt: "BASIC_PIPE", + bbox_detector: "BBOX_DETECTOR", + sam_model_opt: "SAM_MODEL", + segm_detector_opt: "SEGM_DETECTOR", + detailer_hook: "DETAILER_HOOK", + refiner_model: "MODEL", + refiner_clip: "CLIP", + refiner_positive: "CONDITIONING", + refiner_negative: "CONDITIONING", + base_basic_pipe: "BASIC_PIPE", + refiner_basic_pipe: "BASIC_PIPE", + UPSCALER: "UPSCALER", + PK_HOOK: "PK_HOOK", + DETAILER_HOOK: "DETAILER_HOOK", + UPSCALER_HOOK: "UPSCALER_HOOK", + SEGS_HEADER: "SEGS_HEADER", + SEG_ELT: "SEG_ELT", + seg_elt: "SEG_ELT", + cropped_image: "IMAGE", + crop_region: "SEG_ELT_crop_region", + bbox: "SEG_ELT_bbox", + control_net_wrapper: "SEG_ELT_control_net_wrapper", + confidence: "FLOAT", + label: "STRING", + left: "INT", + top: "INT", + right: "INT", + bottom: "INT", + KSAMPLER: "KSAMPLER", + KSAMPLER_ADVANCED: "KSAMPLER_ADVANCED", + selected_value: "*", + selected_label: "STRING", + selected_index: "INT", + output1: "*", + populated_text: "STRING", + BASIC_PIPE: "BASIC_PIPE", + REGIONAL_PROMPTS: "REGIONAL_PROMPTS", + filtered_SEGS: "SEGS", + remained_SEGS: "SEGS", + signal_opt: "*", + bool: "BOOLEAN", + signal: "*", + batch: "INT", + height: "INT", + width: "INT", + channel: "INT", + wildcard: "STRING", + segs_labels: "STRING", + count: "INT", + total: "INT", + value: "*", + TRANSFORMERS_CLASSIFIER: "TRANSFORMERS_CLASSIFIER", + scheduler: [ + "normal", + "karras", + "exponential", + "sgm_uniform", + "simple", + "ddim_uniform", + ], + SEGM_DETECTOR: "SEGM_DETECTOR", + TIMESTEP_KF: "TIMESTEP_KEYFRAME", + LATENT_KF: "LATENT_KEYFRAME", + model_opt: "MODEL", + CN_WEIGHTS: "CONTROL_NET_WEIGHTS", + TK_SHORTCUT: "TIMESTEP_KEYFRAME", + proc_IMAGE: "IMAGE", + SPARSE_METHOD: "SPARSE_METHOD", + cn_extras: "CN_WEIGHTS_EXTRAS", + "ABG_CHARACTER_MASK (MASK)": "MASK", + POSE_KEYPOINT: "POSE_KEYPOINT", + tracking: "TRACKING", + prompt: "STRING", + OPTICAL_FLOW: "OPTICAL_FLOW", + PREVIEW_IMAGE: "IMAGE", + INPAINTING_MASK: "MASK", + preprocessor: [ + "none", + "TilePreprocessor", + "TTPlanet_TileGF_Preprocessor", + "TTPlanet_TileSimple_Preprocessor", + "AnimeFace_SemSegPreprocessor", + "BinaryPreprocessor", + "Zoe-DepthMapPreprocessor", + "ColorPreprocessor", + "DepthAnythingPreprocessor", + "Zoe_DepthAnythingPreprocessor", + "DensePosePreprocessor", + "OneFormer-COCO-SemSegPreprocessor", + "OneFormer-ADE20K-SemSegPreprocessor", + "M-LSDPreprocessor", + "AnyLineArtPreprocessor_aux", + "DWPreprocessor", + "AnimalPosePreprocessor", + "DSINE-NormalMapPreprocessor", + "MediaPipe-FaceMeshPreprocessor", + "LineArtPreprocessor", + "MiDaS-NormalMapPreprocessor", + "MiDaS-DepthMapPreprocessor", + "CannyEdgePreprocessor", + "ShufflePreprocessor", + "Metric3D-DepthMapPreprocessor", + "Metric3D-NormalMapPreprocessor", + "AnimeLineArtPreprocessor", + "SAMPreprocessor", + "HEDPreprocessor", + "FakeScribblePreprocessor", + "ScribblePreprocessor", + "Scribble_XDoG_Preprocessor", + "Scribble_PiDiNet_Preprocessor", + "DiffusionEdge_Preprocessor", + "SavePoseKpsAsJsonFile", + "FacialPartColoringFromPoseKps", + "UpperBodyTrackingFromPoseKps", + "PiDiNetPreprocessor", + "OpenposePreprocessor", + "LeReS-DepthMapPreprocessor", + "Unimatch_OptFlowPreprocessor", + "MaskOptFlow", + "ImageLuminanceDetector", + "ImageIntensityDetector", + "UniFormer-SemSegPreprocessor", + "SemSegPreprocessor", + "MeshGraphormer-DepthMapPreprocessor", + "MeshGraphormer+ImpactDetector-DepthMapPreprocessor", + "Manga2Anime_LineArt_Preprocessor", + "BAE-NormalMapPreprocessor", + "TEEDPreprocessor", + "LineartStandardPreprocessor", + ], + "RESOLUTION (INT)": "INT", + "IMAGE_GEN_WIDTH (INT)": "INT", + "IMAGE_GEN_HEIGHT (INT)": "INT", + "Full prompt": "STRING", + "Short prompt": "STRING", + "Compiled prompt": "CONDITIONING", + Value: "STRING", + Value2: "STRING", + mix: "INT", + Filenames: "VHS_FILENAMES", + frame_count: "INT", + audio: "VHS_AUDIO", + video_info: "VHS_VIDEOINFO", + meta_batch: "VHS_BatchManager", + "source_fps🟨": "FLOAT", + "source_frame_count🟨": "INT", + "source_duration🟨": "FLOAT", + "source_width🟨": "INT", + "source_height🟨": "INT", + "loaded_fps🟦": "FLOAT", + "loaded_frame_count🟦": "INT", + "loaded_duration🟦": "FLOAT", + "loaded_width🟦": "INT", + "loaded_height🟦": "INT", + "fps🟨": "FLOAT", + "frame_count🟨": "INT", + "duration🟨": "FLOAT", + "width🟨": "INT", + "height🟨": "INT", + "fps🟦": "FLOAT", + "frame_count🟦": "INT", + "duration🟦": "FLOAT", + "width🟦": "INT", + "height🟦": "INT", + LATENT_A: "LATENT", + A_count: "INT", + LATENT_B: "LATENT", + B_count: "INT", + IMAGE_A: "IMAGE", + IMAGE_B: "IMAGE", + MASK_A: "MASK", + MASK_B: "MASK", + modelname: "STRING", + sampler_name: [ + "euler", + "euler_ancestral", + "heun", + "heunpp2", + "dpm_2", + "dpm_2_ancestral", + "lms", + "dpm_fast", + "dpm_adaptive", + "dpmpp_2s_ancestral", + "dpmpp_sde", + "dpmpp_sde_gpu", + "dpmpp_2m", + "dpmpp_2m_sde", + "dpmpp_2m_sde_gpu", + "dpmpp_3m_sde", + "dpmpp_3m_sde_gpu", + "ddpm", + "lcm", + "ddim", + "uni_pc", + "uni_pc_bh2", + ], + scheduler_name: "STRING", + s: "STRING", + x: "INT", + y: "INT", + REMBG_SESSION: "REMBG_SESSION", + red: "MASK", + green: "MASK", + blue: "MASK", + cyan: "MASK", + magenta: "MASK", + yellow: "MASK", + black: "MASK", + white: "MASK", + CLIP_SEG: "CLIP_SEG", + description: "STRING", + response: "STRING", + context: "STRING", + }; + + const matchName = nameTypeMap[name]; + if (matchName) { + return matchName; + } else { + console.log("not matching name, will return unknown: ", name); + return "XXX UNKNOWN INPUT TYPE XXX"; + } +} + +// originally was in in file that imported app +function scanNodeInputOutput() { + const nameTypeMap = new Map(); + + function updateNameTypeMap(input, output) { + const maxLength = Math.min(input.length, output.length); + + for (let i = 0; i < maxLength; i++) { + const inputName = input[i]; + const outputName = output[i]; + + // Add to nameTypeMap if inputName is not already present + if (!nameTypeMap.has(inputName)) { + nameTypeMap[inputName] = outputName; + } + } + } + + + console.log(LiteGraph.registered_node_types); + if (LiteGraph.registered_node_types) { + // Get the first node in the list + for (const [key, nodeType] of Object.entries( + LiteGraph.registered_node_types + )) { + console.log(key); + if (nodeType.nodeData?.output !== undefined) { + updateNameTypeMap( + nodeType.nodeData.output_name, + nodeType.nodeData.output + ); + } + } + console.log(JSON.stringify(nameTypeMap)); + } else { + console.log("No registered nodes found."); + } + return nameTypeMap; +} diff --git a/js/chatbot/ext.js b/js/chatbot/ext.js new file mode 100644 index 0000000..ce9010f --- /dev/null +++ b/js/chatbot/ext.js @@ -0,0 +1,275 @@ +// import { api, app } from './comfy/comfy.js' +import { app } from "../comfy/comfy.js"; +import { + getBotResponse, + isValidWorkflow, + parseWorkflowFromBot, +} from "./chatbot.js"; +import { loadGraphFromPrompt } from "./workflow.js"; +import { setButtonDefault } from "../button/ui.js"; +import { authDialog } from "../auth/index.js"; +import { getApiToken } from "../utils.js"; + +const CHAT_BUTTON_ID = "chat-button"; +const CHAT_BOX_ID = "chat-box"; +const CHAT_INPUT_ID = "chat-input"; +const CHAT_MESSAGES_ID = "chat-messages"; +const CHAT_SEND_BUTTON_ID = "chat-send-button"; + +const apiToken = getApiToken(); +const doesApiTokenExist = !!apiToken; + +let canSendMessage = true; + +const toggleChatBox = async () => { + const chatBox = document.getElementById(CHAT_BOX_ID); + if (chatBox.style.display === "none") { + chatBox.style.display = "block"; + } else { + chatBox.style.display = "none"; + } +}; + +const createChatButton = () => { + const chatButton = document.createElement("div"); + chatButton.id = CHAT_BUTTON_ID; + + // Set up the button styles and gradient background + chatButton.style.position = "fixed"; + chatButton.style.bottom = "0"; + chatButton.style.right = "0"; + chatButton.style.margin = "20px"; + chatButton.style.background = "linear-gradient(160deg, #8A2BE2, #1E90FF)"; // Gradient background + chatButton.style.color = "white"; + chatButton.style.borderRadius = "50%"; + chatButton.style.width = "60px"; + chatButton.style.height = "60px"; + chatButton.style.zIndex = 9999; + chatButton.style.display = "flex"; + chatButton.style.alignItems = "center"; + chatButton.style.justifyContent = "center"; + chatButton.style.cursor = "pointer"; + + // Create and append SVG chat bubble icon + const chatIcon = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + chatIcon.setAttribute("viewBox", "0 0 512 512"); + chatIcon.setAttribute("width", "20"); // Adjust size as needed + chatIcon.setAttribute("height", "20"); // Adjust size as needed + chatIcon.setAttribute("fill", "currentColor"); // Use current text color + + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute( + "d", + "M416,0H96C43.072,0,0,43.072,0,96v405.333c0,4.48,2.816,8.491,7.04,10.027c1.195,0.427,2.411,0.64,3.627,0.64c3.115,0,6.123-1.344,8.192-3.84L122.325,384H416c52.928,0,96-43.072,96-96V96C512,43.072,468.928,0,416,0z" + ); + chatIcon.appendChild(path); + + chatButton.appendChild(chatIcon); + + return chatButton; +}; + +const createChatBox = () => { + const chatBox = document.createElement("div"); + chatBox.id = CHAT_BOX_ID; + chatBox.style.position = "fixed"; + chatBox.style.bottom = "100px"; + chatBox.style.right = "0"; + chatBox.style.backgroundColor = "white"; + chatBox.style.width = "300px"; + chatBox.style.height = "500px"; + chatBox.style.zIndex = 9999; + chatBox.style.borderRadius = "10px"; + chatBox.style.display = "none"; // Initially hidden + + // Messages section + const chatMessages = document.createElement("div"); + chatMessages.id = CHAT_MESSAGES_ID; + chatMessages.style.height = "80%"; + chatMessages.style.overflowY = "auto"; + chatMessages.style.padding = "10px"; + chatMessages.style.color = "black"; + chatBox.appendChild(chatMessages); + // Input section + const chatInputContainer = document.createElement("div"); + chatInputContainer.style.position = "absolute"; + chatInputContainer.style.bottom = "0"; + chatInputContainer.style.width = "100%"; + chatInputContainer.style.padding = "10px"; + chatInputContainer.style.boxSizing = "border-box"; + chatInputContainer.style.backgroundColor = "#f1f1f1"; + chatInputContainer.style.borderBottomLeftRadius = "10px"; + chatInputContainer.style.borderBottomRightRadius = "10px"; + + // Flexbox layout for horizontal alignment + chatInputContainer.style.display = "flex"; + chatInputContainer.style.alignItems = "center"; + + // Input field + const chatInput = document.createElement("input"); + chatInput.id = CHAT_INPUT_ID; + chatInput.type = "text"; + chatInput.style.flex = "1"; // Allow the input to grow and take up available space + chatInput.style.padding = "10px"; + chatInput.style.boxSizing = "border-box"; + chatInput.style.border = "1px solid #ccc"; + chatInput.style.borderRadius = "4px"; + chatInput.placeholder = "Describe your desired workflow..."; + + // Send button + const chatSendButton = document.createElement("button"); + chatSendButton.id = CHAT_SEND_BUTTON_ID; + chatSendButton.style.width = "40px"; + chatSendButton.style.height = "40px"; + chatSendButton.style.marginLeft = "10px"; + chatSendButton.style.padding = "0"; // Remove default padding + chatSendButton.style.border = "none"; + chatSendButton.style.backgroundColor = "#007acc"; + chatSendButton.style.color = "white"; + chatSendButton.style.cursor = "pointer"; + chatSendButton.style.borderRadius = "50%"; + chatSendButton.style.display = "flex"; + chatSendButton.style.alignItems = "center"; + chatSendButton.style.justifyContent = "center"; + + // Create and style the SVG icon + const sendIcon = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + sendIcon.setAttribute("fill", "#ffffff"); // Use white color for the icon + sendIcon.setAttribute("viewBox", "0 0 52 52"); + sendIcon.setAttribute("enable-background", "new 0 0 52 52"); + sendIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + sendIcon.setAttribute("class", "send-icon"); + sendIcon.setAttribute("xml:space", "preserve"); + sendIcon.setAttribute("width", "18px"); // Adjust size as needed + sendIcon.setAttribute("height", "18px"); // Adjust size as needed + + // Create the path element for the SVG + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute( + "d", + "M2.1,44.5l4.4-16.3h18.6c0.5,0,1-0.5,1-1v-2c0-0.5-0.5-1-1-1H6.5l-4.3-16l0,0C2.1,8,2,7.7,2,7.4C2,6.7,2.7,6,3.5,6.1c0.2,0,0.3,0.1,0.5,0.1l0,0l0,0l0,0l0,0l45,18.5c0.6,0.2,1,0.8,1,1.4s-0.4,1.1-0.9,1.3l0,0L4,46.4l0,0c-0.2,0.1-0.4,0.1-0.6,0.1C2.6,46.4,2,45.8,2,45C2,44.8,2,44.7,2.1,44.5L2.1,44.5z" + ); + sendIcon.appendChild(path); + + // Append SVG to button + chatSendButton.appendChild(sendIcon); + + // Add event listener + chatSendButton.addEventListener("click", sendMessage); + + // Append input and button to container + chatInputContainer.appendChild(chatInput); + chatInputContainer.appendChild(chatSendButton); + chatBox.appendChild(chatInputContainer); + + return chatBox; +}; + +const updateMessageBoxStatus = (canSendMessage) => { + const button = document.getElementById(CHAT_SEND_BUTTON_ID); + if (button) { + button.style.backgroundColor = canSendMessage ? "#007acc" : "#cccccc"; // Change color based on `isActive` + } + const input = document.getElementById(CHAT_INPUT_ID); + if (input) { + input.placeholder = canSendMessage ? "Describe your desired workflow..." : "Generating workflow..."; + input.disabled = !canSendMessage; + } +}; + +const sendMessage = async () => { + if (!doesApiTokenExist) { + toggleChatBox(); + setButtonDefault(); + return authDialog.show(); + } + + const chatInput = document.getElementById(CHAT_INPUT_ID); + const chatMessages = document.getElementById(CHAT_MESSAGES_ID); + const message = chatInput.value.trim(); + + if (message) { + if (!canSendMessage) { + return; + } else { + canSendMessage = false; + updateMessageBoxStatus(canSendMessage); + } + + const messageElement = document.createElement("div"); + messageElement.innerText = message; + messageElement.style.marginBottom = "10px"; + messageElement.style.padding = "10px"; + messageElement.style.backgroundColor = "#A7DBFD"; + messageElement.style.borderRadius = "4px"; + + chatMessages.appendChild(messageElement); + chatMessages.scrollTop = chatMessages.scrollHeight; // Scroll to the bottom + chatInput.value = ""; + + try { + const response = await getBotResponse(message); + let botMessage = ""; + + const parsedBotResponse = parseWorkflowFromBot(response); + + if (isValidWorkflow(parsedBotResponse)) { + botMessage = JSON.stringify(parsedBotResponse); + } else { + botMessage = response.responses.bot; + } + + const botMessageElement = document.createElement("div"); + botMessageElement.innerText = botMessage; + botMessageElement.style.marginBottom = "10px"; + botMessageElement.style.padding = "10px"; + botMessageElement.style.backgroundColor = "#DFE8EE"; + botMessageElement.style.borderRadius = "4px"; + botMessageElement.style.wordWrap = "break-word"; // Wrap long words + botMessageElement.style.overflow = "hidden"; // Hide overflow text + botMessageElement.style.textOverflow = "ellipsis"; // Show ellipsis if text overflows + + // Optionally, set a max-width if you want to control the width of the message box + botMessageElement.style.maxWidth = "calc(100% - 20px)"; // Adjust max-width based on container width and padding + + chatMessages.appendChild(botMessageElement); + chatMessages.scrollTop = chatMessages.scrollHeight; // Scroll to the bottom + + loadGraphFromPrompt(parsedBotResponse); + } catch (error) { + console.error(error); + } finally { + canSendMessage = true; + updateMessageBoxStatus(canSendMessage); + } + } +}; + +const registerChat = () => { + // With js, append an bottom that opens up a chatbox in the bottom right hand corner + const chatButton = createChatButton(); + const chatBox = createChatBox(); + + document.body.appendChild(chatBox); + document.body.appendChild(chatButton); + chatButton.addEventListener("click", toggleChatBox); +}; + +/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ +/** @type {ComfyExtension} */ +const ext = { + name: "nathannlu.ComfyUI-Chat", + + // ComfyUI extension init + init() { + registerChat(); + }, +}; + +app.registerExtension(ext); diff --git a/js/chatbot/workflow.js b/js/chatbot/workflow.js new file mode 100644 index 0000000..3ad7155 --- /dev/null +++ b/js/chatbot/workflow.js @@ -0,0 +1,116 @@ +import { app } from "../comfy/comfy.js"; +import { getInputType } from "./edgeTypes.js"; + +function getInputIndex(node, inputName) { + return node.inputs.findIndex( + (input) => input.name === getInputType(inputName) + ); +} + +// translates chatgpt input to immediately loadable graph format +function transformInputToGraph(input) { + const baseGraph = { + last_node_id: input.nodes.length, + last_link_id: input.edges.length, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0.4, + }; + + + const xStep = 300; // step for node positions + const yStep = 300; // step for node positions + const yBase = 100; // base y position + const nodesById = {}; // pointer of nodes by id for easier/faster search + + let x = 100; // starting x post + let y = yBase; // starting y post + let columnCounter = 0; // counter for columns + // Create nodes with positions in order + input.nodes.forEach((node, index) => { + const newNode = { + id: node.id, + type: node.class_type, + pos: [x, y], + size: [200, 50], // TODO: write a function to get the node's actual height for better placement + flags: {}, + order: index, + mode: 0, + inputs: [], + outputs: [], + properties: {}, + widgets_values: node.widget_values ? Object.values(node.widget_values) : [] + }; + + baseGraph.nodes.push(newNode); + nodesById[node.id] = newNode; + + if (columnCounter % 3 === 0) { + x += xStep; + y = yBase; + } else { + // y += newNode.size[1] + yStep; + y += yStep; + } + columnCounter++; + }); + + // Insert edges + input.edges.forEach((edgeData, index) => { + const fromNode = nodesById[edgeData.from]; + const toNode = nodesById[edgeData.to]; + + // Adding output to fromNode + let outputIndex = edgeData.from_output; + // add inputs + let inputIndex = getInputIndex(toNode, edgeData.to_input); + + // if input doesnt already exist + if (inputIndex < 0) { + toNode.inputs.push({ + name: edgeData.to_input, + type: getInputType(edgeData.to_input), + link: index + 1, + }); + } + + // add outputs + fromNode.outputs.push({ + name: getInputType(edgeData.to_input), + // name: edgeData.input, + type: getInputType(edgeData.to_input), + links: [], + slot_index: fromNode.outputs.length, + }); + + + outputIndex = fromNode.outputs.length - 1; + inputIndex = fromNode.inputs.length - 1; + + // add links + const link = [ + index + 1, // Link id + fromNode.id, // Origin node id + edgeData.from_output, + // getOutputIndex(fromNode, edgeData.to_input), // origin index + toNode.id, // Destination node id + getInputIndex(toNode, edgeData.to_input), // destination index + getInputType(edgeData.to_input), + ]; + + fromNode.outputs[outputIndex].links.push(link[0]); + baseGraph.links.push(link); + }); + + + return baseGraph; +} + +export function loadGraphFromPrompt(generatedWorkflow) { + const translatedData = transformInputToGraph(generatedWorkflow); + app.loadGraphData(translatedData, true); + app.graph.change(); +} diff --git a/js/resource/endpoints.js b/js/resource/endpoints.js index c638217..2cf2d3f 100644 --- a/js/resource/endpoints.js +++ b/js/resource/endpoints.js @@ -17,14 +17,14 @@ const workflowRun = { retrieveOutput: { method: "GET", path: "/workflow/{workflow_id}/runs/{run_id}/outputs", - } -} + }, +}; const workflow = { // upload local init: { method: "POST", - path: "/workflow/init" + path: "/workflow/init", }, create: { method: "POST", @@ -40,71 +40,75 @@ const workflow = { }, error: { method: "POST", - path: "/workflow/error" - } -} + path: "/workflow/error", + }, +}; const billing = { retrieveCustomerSession: { method: "GET", - path: "/stripe/get-customer-session" + path: "/stripe/get-customer-session", }, retrieveUsage: { method: "GET", - path: "/stripe/usage" + path: "/stripe/usage", }, retrieveCredits: { method: "GET", - path: "/stripe/credits" + path: "/stripe/credits", }, -} +}; const auth = { register: { method: "POST", - path: "/auth/register" + path: "/auth/register", }, login: { method: "POST", - path: "/auth/login" - } -} + path: "/auth/login", + }, +}; const support = { feedback: { method: "POST", - path: "/support/feedback" + path: "/support/feedback", }, support: { method: "POST", - path: "/support/support" + path: "/support/support", }, docs: { method: "POST", - path: "/support/docs" + path: "/support/docs", }, tooltipHover: { method: "POST", - path: "/support/tooltipHover" + path: "/support/tooltipHover", }, tooltipDocs: { method: "POST", - path: "/support/tooltipDocs" + path: "/support/tooltipDocs", }, assistant: { method: "POST", - path: "/support/assistant" - } -} + path: "/support/assistant", + }, +}; +const chatbot = { + sendMessage: { + method: "POST", + path: "/chat/send-message", + }, +}; export const apiEndpoints = { workflowRun, workflow, billing, auth, - support -} - - - + support, + chatbot +};