diff --git a/web/js/appearance.js b/web/js/appearance.js
index 72fce90..eca72d7 100644
--- a/web/js/appearance.js
+++ b/web/js/appearance.js
@@ -1,50 +1,54 @@
-import { app } from "/scripts/app.js";
-
-const COLOR_THEMES = {
- QwenVL: { nodeColor: "#28403f", nodeBgColor: "#374539", width: 340 },
- QwenVLGGUF: { nodeColor: "#474539", nodeBgColor: "#2c4045", width: 340 },
- Tools: { nodeColor: "#28403f", nodeBgColor: "#233238", width: 300 },
- Enhancer: { nodeColor: "#374445", nodeBgColor: "#474539", width: 340 },
-};
-
-const NODE_COLORS = {
- // QwenVL nodes
- "AILab_QwenVL": "QwenVL",
- "AILab_QwenVL_Advanced": "QwenVL",
- "AILab_QwenVL_PromptEnhancer": "Enhancer",
- "AILab_QwenVL_GGUF": "QwenVLGGUF",
- "AILab_QwenVL_GGUF_Advanced": "QwenVLGGUF",
- "AILab_QwenVL_GGUF_PromptEnhancer": "Enhancer",
-
- // Tools
- "AILab_QwenVL_PromptLibrary": "Tools",
-};
-
-function setNodeColors(node, theme) {
- if (!theme) { return; }
- if (theme.nodeColor) {
- node.color = theme.nodeColor;
- }
- if (theme.nodeBgColor) {
- node.bgcolor = theme.nodeBgColor;
- }
- if (theme.width) {
- node.size = node.size || [140, 80];
- node.size[0] = theme.width;
- }
-}
-
-const ext = {
- name: "QwenVL.appearance",
-
- nodeCreated(node) {
- const nclass = node.comfyClass;
- if (NODE_COLORS.hasOwnProperty(nclass)) {
- let colorKey = NODE_COLORS[nclass];
- const theme = COLOR_THEMES[colorKey];
- setNodeColors(node, theme);
- }
- }
-};
-
+import { app } from "/scripts/app.js";
+
+const COLOR_THEMES = {
+ QwenVL: { nodeColor: "#28403f", nodeBgColor: "#28403f", width: 340 },
+ QwenVLGGUF: { nodeColor: "#474539", nodeBgColor: "#474539", width: 340 },
+ Tools: { nodeColor: "#28403f", nodeBgColor: "#233238", width: 420 },
+ Enhancer: { nodeColor: "#374445", nodeBgColor: "#474539", width: 340 },
+};
+
+const NODE_COLORS = {
+ // QwenVL nodes
+ "AILab_QwenVL": "QwenVL",
+ "AILab_QwenVL_Advanced": "QwenVL",
+ "AILab_QwenVL_PromptEnhancer": "Enhancer",
+ "AILab_QwenVL_GGUF": "QwenVLGGUF",
+ "AILab_QwenVL_GGUF_Advanced": "QwenVLGGUF",
+ "AILab_QwenVL_GGUF_PromptEnhancer": "Enhancer",
+ "AILab_QwenVL_VideoPromptEnhancer": "Enhancer",
+ "AILab_QwenVL_MiniMax_H3_Director": "Enhancer",
+ "AILab_QwenVL_LTX_Video_Enhancer": "Enhancer",
+
+ // Tools
+ "AILab_QwenVL_PromptLibrary": "Tools",
+ "AILab_HuggingFaceDownloader": "Tools",
+};
+
+function setNodeColors(node, theme) {
+ if (!theme) { return; }
+ if (theme.nodeColor) {
+ node.color = theme.nodeColor;
+ }
+ if (theme.nodeBgColor) {
+ node.bgcolor = theme.nodeBgColor;
+ }
+ if (theme.width) {
+ node.size = node.size || [140, 80];
+ node.size[0] = theme.width;
+ }
+}
+
+const ext = {
+ name: "QwenVL.appearance",
+
+ nodeCreated(node) {
+ const nclass = node.comfyClass;
+ if (NODE_COLORS.hasOwnProperty(nclass)) {
+ let colorKey = NODE_COLORS[nclass];
+ const theme = COLOR_THEMES[colorKey];
+ setNodeColors(node, theme);
+ }
+ }
+};
+
app.registerExtension(ext);
\ No newline at end of file
diff --git a/web/js/downloader.js b/web/js/downloader.js
new file mode 100644
index 0000000..cfd8281
--- /dev/null
+++ b/web/js/downloader.js
@@ -0,0 +1,199 @@
+import { app } from "/scripts/app.js";
+
+function escapeHtml(str) {
+ if (!str) return "";
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function buildRichSummaryHTML(data, rawText) {
+ if (data && data.status === "success") {
+ const filesListHtml = (data.files || [])
+ .map((f) => `
• ${escapeHtml(f)}
`)
+ .join("");
+
+ let regHtml = "";
+ if (data.registration && data.registration.entry) {
+ const regSnippet = JSON.stringify({ [data.registration.key]: data.registration.entry }, null, 2);
+ regHtml = `
+
+
• Registered in custom_models.json [${escapeHtml(data.registration.section)}]:
+
${escapeHtml(regSnippet)}
+
+ `;
+ }
+
+ return `
+
+
+
+ 📥 QwenVL Downloader Summary
+
+
+ ✅ Completed
+
+
+
+ • Repository:
+ ${escapeHtml(data.repo_id)}
+
+
+ • Save Location:
+ ${escapeHtml(data.save_folder)}
+
+
+
• Downloaded Files:
+
${filesListHtml}
+
+ ${regHtml}
+
+ `;
+ } else if (data && data.status === "error") {
+ return `
+
+
+ ❌ Download Failed
+ Error
+
+
+ • Repository:
+ ${escapeHtml(data.repo_id || "")}
+
+
+ ${escapeHtml(data.error || "Unknown error occurred")}
+
+
+ `;
+ }
+
+ // Fallback: parse raw text with highlighting
+ const lines = (rawText || "").split("\n");
+ let resultHtml = "";
+ for (let line of lines) {
+ if (line.startsWith("==")) continue;
+ if (line.includes("Download Summary")) {
+ resultHtml += `${escapeHtml(line)}
`;
+ } else if (line.includes("Status:") && line.includes("Completed")) {
+ resultHtml += `• Status: ✅ Completed Successfully
`;
+ } else if (line.startsWith("• Repository:")) {
+ resultHtml += `• Repository: ${escapeHtml(line.replace("• Repository:", "").trim())}
`;
+ } else if (line.startsWith("• Save Location:")) {
+ resultHtml += `• Save Location: ${escapeHtml(line.replace("• Save Location:", "").trim())}
`;
+ } else if (line.startsWith("• Downloaded Files:")) {
+ resultHtml += `• Downloaded Files:
`;
+ } else if (line.trim().startsWith("- ")) {
+ resultHtml += `${escapeHtml(line.trim())}
`;
+ } else if (line.includes("Registered to custom_models.json")) {
+ resultHtml += `${escapeHtml(line)}
`;
+ } else {
+ resultHtml += `${escapeHtml(line)}
`;
+ }
+ }
+ return `${resultHtml}
`;
+}
+
+const BASE_INPUTS_HEIGHT = 184;
+const INITIAL_NODE_HEIGHT = 188;
+
+function syncContentSize(node, size) {
+ const domWidget = node.widgets?.find((w) => w.name === "download_display");
+ if (domWidget && domWidget.element) {
+ const nodeH = size ? size[1] : (node.size ? node.size[1] : INITIAL_NODE_HEIGHT);
+ const availableHeight = Math.max(120, Math.round(nodeH - BASE_INPUTS_HEIGHT));
+ domWidget.element.style.height = `${availableHeight}px`;
+ }
+}
+
+app.registerExtension({
+ name: "AILab.HuggingFaceDownloader",
+
+ nodeCreated(node) {
+ if (node.comfyClass === "AILab_HuggingFaceDownloader") {
+ requestAnimationFrame(() => {
+ node.size = [Math.max(node.size?.[0] || 420, 420), INITIAL_NODE_HEIGHT];
+ if (typeof node.setSize === "function") {
+ node.setSize(node.size);
+ }
+ app.graph?.setDirtyCanvas(true, true);
+ });
+ }
+ },
+
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "AILab_HuggingFaceDownloader") {
+ const origOnResize = nodeType.prototype.onResize;
+ nodeType.prototype.onResize = function (size) {
+ origOnResize?.apply(this, arguments);
+ size[0] = Math.max(size[0], 360);
+ size[1] = Math.max(size[1], INITIAL_NODE_HEIGHT);
+ syncContentSize(this, size);
+ };
+
+ const onExecuted = nodeType.prototype.onExecuted;
+ nodeType.prototype.onExecuted = function (message) {
+ onExecuted?.apply(this, arguments);
+
+ const rawText = Array.isArray(message?.text) ? message.text.join("\n") : (message?.text || "");
+ const summaryData = Array.isArray(message?.summary) ? message.summary[0] : message?.summary;
+
+ if (rawText || summaryData) {
+ const richHtml = buildRichSummaryHTML(summaryData, rawText);
+
+ let domWidget = this.widgets?.find((w) => w.name === "download_display");
+
+ if (!domWidget) {
+ const container = document.createElement("div");
+ container.className = "ailab-download-display-container";
+ container.style.boxSizing = "border-box";
+ container.style.width = "100%";
+ container.style.height = "100%";
+ container.style.margin = "0";
+ container.style.padding = "0 2px 4px 2px";
+ container.style.userSelect = "text";
+ container.style.cursor = "auto";
+ container.style.display = "flex";
+ container.style.flexDirection = "column";
+ container.style.overflow = "hidden";
+
+ domWidget = this.addDOMWidget("download_display", "display_element", container, {
+ getValue() {
+ return container.innerHTML;
+ },
+ setValue(val) {
+ container.innerHTML = val;
+ },
+ });
+ domWidget.element = container;
+ domWidget.serialize = false;
+ if (domWidget.options) {
+ domWidget.options.serialize = false;
+ }
+ }
+
+ if (domWidget && domWidget.element) {
+ domWidget.element.innerHTML = richHtml;
+
+ requestAnimationFrame(() => {
+ const card = domWidget.element.querySelector(".ailab-download-card") || domWidget.element;
+ const cardH = card ? (card.scrollHeight || card.offsetHeight) : 260;
+ const targetHeight = Math.ceil(BASE_INPUTS_HEIGHT + cardH + 10);
+ const targetWidth = Math.max(this.size ? this.size[0] : 420, 420);
+
+ this.size = [targetWidth, targetHeight];
+ syncContentSize(this, this.size);
+ if (typeof this.setSize === "function") {
+ this.setSize([targetWidth, targetHeight]);
+ }
+ this.setDirtyCanvas?.(true, true);
+ app.graph?.setDirtyCanvas(true, true);
+ });
+ }
+ }
+ };
+ }
+ },
+});