diff --git a/README.md b/README.md index ecaf9e4..0382fd0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ A versatile ComfyUI custom node that renders HTML, SVG, Markdown, and code content in a secure embedded iframe viewer with full editing, download, and list support. +
+Screenshots + +![Screenshot 1](screenshots/screenshot_1.jpg) +![Screenshot 2](screenshots/screenshot_2.gif) +![Screenshot 3](screenshots/screenshot_3.jpg) +![Screenshot 4](screenshots/screenshot_4.jpg) +![Screenshot 5](screenshots/screenshot_5.jpg) + +
+ ## Features ### Content Rendering diff --git a/__init__.py b/__init__.py index 111379f..8fbecc2 100644 --- a/__init__.py +++ b/__init__.py @@ -19,7 +19,7 @@ class WASComfyViewer: }, "hidden": { "manual_content": ("STRING", {"default": ""}), - "excluded_indices": ("STRING", {"default": "[]"}), + "viewer_meta": ("STRING", {"default": "{}"}), }, } @@ -31,7 +31,7 @@ class WASComfyViewer: FUNCTION = "run" CATEGORY = "WAS/View" - def run(self, content=None, manual_content=None, excluded_indices=None): + def run(self, content=None, manual_content=None, viewer_meta=None): import json import logging @@ -65,14 +65,12 @@ class WASComfyViewer: manual_content = [manual_content] excluded = [] - if excluded_indices: - excluded_str = excluded_indices[0] if isinstance(excluded_indices, list) else excluded_indices + if viewer_meta: + meta_str = viewer_meta[0] if isinstance(viewer_meta, list) else viewer_meta try: - parsed = json.loads(excluded_str) + parsed = json.loads(meta_str) if isinstance(parsed, dict) and "excluded" in parsed: excluded = parsed["excluded"] if isinstance(parsed["excluded"], list) else [] - elif isinstance(parsed, list): - excluded = parsed except: excluded = [] @@ -81,23 +79,29 @@ class WASComfyViewer: logger.info(f"\n[WAS Viewer] Content:\n{content_trimmed}\nManual Content:\n{manual_content_trimmed}\nExcluded: {excluded}\n") - if len(content) > 0 and any(c for c in content): + LIST_SEPARATOR = "\n---LIST_SEPARATOR---\n" + + if len(manual_content) > 0 and any(m for m in manual_content): + combined = to_string(manual_content[0]) if len(manual_content) == 1 else LIST_SEPARATOR.join(to_string(m) for m in manual_content) + values = combined.split(LIST_SEPARATOR) if LIST_SEPARATOR in combined else [combined] + logger.info(f"[WAS Viewer] Using manual_content: {len(values)} items") + elif len(content) > 0 and any(c for c in content): values = [to_string(c) for c in content] logger.info(f"[WAS Viewer] Using content input: {len(values)} items") - elif len(manual_content) > 0 and any(m for m in manual_content): - values = [to_string(m) for m in manual_content] - logger.info(f"[WAS Viewer] Using manual_content: {len(values)} items") else: values = [""] logger.info("[WAS Viewer] No content, using empty") - display_text = "\n---LIST_SEPARATOR---\n".join(values) + display_text = LIST_SEPARATOR.join(values) + + source_content = LIST_SEPARATOR.join(to_string(c) for c in content) if content else "" + content_hash = str(len(source_content)) + "_" + str(hash(source_content) & 0xFFFFFFFF) output_values = [v for i, v in enumerate(values) if i not in excluded] if not output_values: output_values = [""] - return {"ui": {"text": (display_text,)}, "result": (output_values,)} + return {"ui": {"text": (display_text,), "source_content": (source_content,), "content_hash": (content_hash,)}, "result": (output_values,)} NODE_CLASS_MAPPINGS = { diff --git a/pyproject.toml b/pyproject.toml index 3c8d02e..8a25b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI_Viewer" -version = "0.0.2" +version = "0.0.3" description = "ComfyUI Content Viewer - Renders HTML, Markdown, and code in an iframe" readme = "README.md" requires-python = ">=3.10" diff --git a/screenshots/screenshot_1.jpg b/screenshots/screenshot_1.jpg new file mode 100644 index 0000000..80705de Binary files /dev/null and b/screenshots/screenshot_1.jpg differ diff --git a/screenshots/screenshot_2.gif b/screenshots/screenshot_2.gif new file mode 100644 index 0000000..e3869b6 Binary files /dev/null and b/screenshots/screenshot_2.gif differ diff --git a/screenshots/screenshot_3.jpg b/screenshots/screenshot_3.jpg new file mode 100644 index 0000000..d7d672d Binary files /dev/null and b/screenshots/screenshot_3.jpg differ diff --git a/screenshots/screenshot_4.jpg b/screenshots/screenshot_4.jpg new file mode 100644 index 0000000..f46d12e Binary files /dev/null and b/screenshots/screenshot_4.jpg differ diff --git a/screenshots/screenshot_5.jpg b/screenshots/screenshot_5.jpg new file mode 100644 index 0000000..72f77f6 Binary files /dev/null and b/screenshots/screenshot_5.jpg differ diff --git a/web/comfy_viewer.js b/web/comfy_viewer.js index 302c9e5..7b16904 100644 --- a/web/comfy_viewer.js +++ b/web/comfy_viewer.js @@ -47,6 +47,16 @@ async function loadPrismScripts() { loadPrismScripts(); +function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return Math.abs(hash).toString(36); +} + function readCssVar(style, name) { const v = style.getPropertyValue(name); return v ? String(v).trim() : ""; @@ -797,7 +807,7 @@ function getConnectedContent(node) { function getNodeContent(node, elements) { const manualContent = getWidgetValue(node, "manual_content"); - if (manualContent && (elements?.hasBackendContent || elements?.hasUserEdits)) { + if (manualContent) { return manualContent; } @@ -806,7 +816,7 @@ function getNodeContent(node, elements) { return connectedContent; } - return manualContent || ""; + return ""; } function createControlsBar(node, elements) { @@ -857,33 +867,30 @@ function createControlsBar(node, elements) { toggleAllBtn.onclick = () => { const content = getNodeContent(node, elements); - const contentHash = content ? content.length + "_" + content.slice(0, 100) : ""; - const widget = node.widgets?.find(w => w.name === "excluded_indices"); - if (!widget) return; + const metaWidget = node.widgets?.find(w => w.name === "viewer_meta"); + if (!metaWidget) return; - let data = { hash: contentHash, excluded: [] }; + let meta = { lastInputHash: "", excluded: [] }; try { - const parsed = JSON.parse(widget.value || "{}"); - if (parsed && typeof parsed === "object" && parsed.hash !== undefined) { - data = parsed; - } + meta = JSON.parse(metaWidget.value || "{}"); + if (!Array.isArray(meta.excluded)) meta.excluded = []; } catch {} const LIST_SEPARATOR = "\n---LIST_SEPARATOR---\n"; const items = content.split(LIST_SEPARATOR); - const allChecked = data.excluded.length === 0; + const allChecked = meta.excluded.length === 0; if (allChecked) { - data.excluded = items.map((_, i) => i); + meta.excluded = items.map((_, i) => i); toggleAllBtn.textContent = "☐"; toggleAllBtn.title = "Check All"; } else { - data.excluded = []; + meta.excluded = []; toggleAllBtn.textContent = "☑"; toggleAllBtn.title = "Uncheck All"; } - widget.value = JSON.stringify(data); + metaWidget.value = JSON.stringify(meta); elements.lastContentHash = ""; node.setDirtyCanvas?.(true, true); updateIframeContent(node, elements); @@ -919,12 +926,11 @@ function createControlsBar(node, elements) { elements.textarea.style.display = "none"; } setWidgetValue(node, "manual_content", newContent); - elements.hasBackendContent = false; - elements.hasUserEdits = true; elements.lastContentHash = ""; elements.isEditing = false; elements.iframe.style.display = "block"; editBtn.textContent = "Edit"; + node.setDirtyCanvas?.(true, true); app.graph?.change?.(); updateIframeContent(node, elements); @@ -1197,8 +1203,6 @@ function ensureElementsForNode(node) { toggleAllBtn: null, lastContentHash: "", isEditing: false, - hasBackendContent: false, - hasUserEdits: false, listEditContainer: null, listTextareas: null, }; @@ -1359,17 +1363,20 @@ function updateIframeContent(node, elements) { const LIST_SEPARATOR = "\n---LIST_SEPARATOR---\n"; const isListContent = content && content.includes(LIST_SEPARATOR); + let excluded = []; + const metaWidget = node.widgets?.find(w => w.name === "viewer_meta"); + if (metaWidget?.value) { + try { + const meta = JSON.parse(metaWidget.value); + excluded = Array.isArray(meta.excluded) ? meta.excluded : []; + } catch {} + } + if (elements.toggleAllBtn) { elements.toggleAllBtn.style.display = isListContent ? "block" : "none"; if (isListContent) { - const excludedWidget = node.widgets?.find(w => w.name === "excluded_indices"); - let excludedCount = 0; - try { - const parsed = JSON.parse(excludedWidget?.value || "{}"); - if (parsed?.excluded) excludedCount = parsed.excluded.length; - } catch {} - elements.toggleAllBtn.textContent = excludedCount === 0 ? "☑" : "☐"; - elements.toggleAllBtn.title = excludedCount === 0 ? "Uncheck All" : "Check All"; + elements.toggleAllBtn.textContent = excluded.length === 0 ? "☑" : "☐"; + elements.toggleAllBtn.title = excluded.length === 0 ? "Uncheck All" : "Check All"; } } @@ -1378,28 +1385,6 @@ function updateIframeContent(node, elements) { elements.iframe.srcdoc = emptyHtml; return; } - - let excluded = []; - const excludedWidget = node.widgets?.find(w => w.name === "excluded_indices"); - if (excludedWidget?.value) { - try { - const parsed = JSON.parse(excludedWidget.value); - if (parsed && typeof parsed === "object" && parsed.hash !== undefined) { - if (parsed.hash === contentHash) { - excluded = Array.isArray(parsed.excluded) ? parsed.excluded : []; - } else { - excludedWidget.value = JSON.stringify({ hash: contentHash, excluded: [] }); - } - } else if (Array.isArray(parsed)) { - excluded = parsed; - excludedWidget.value = JSON.stringify({ hash: contentHash, excluded: parsed }); - } - } catch { - excludedWidget.value = JSON.stringify({ hash: contentHash, excluded: [] }); - } - } else if (excludedWidget) { - excludedWidget.value = JSON.stringify({ hash: contentHash, excluded: [] }); - } const html = buildIframeContent(content, contentType, theme, excluded); const needsBlobUrl = contentType === "html" && ( @@ -1434,24 +1419,19 @@ window.addEventListener("message", (event) => { if (elements.iframe?.contentWindow === event.source) { const node = app.graph?.getNodeById(parseInt(nodeId)); if (node) { - const widget = node.widgets?.find(w => w.name === "excluded_indices"); - if (widget) { - let data = { hash: "", excluded: [] }; + const metaWidget = node.widgets?.find(w => w.name === "viewer_meta"); + if (metaWidget) { + let meta = { lastInputHash: "", excluded: [] }; try { - const parsed = JSON.parse(widget.value || "{}"); - if (parsed && typeof parsed === "object" && parsed.hash !== undefined) { - data = parsed; - } else if (Array.isArray(parsed)) { - data.excluded = parsed; - } + meta = JSON.parse(metaWidget.value || "{}"); + if (!Array.isArray(meta.excluded)) meta.excluded = []; } catch {} - if (!Array.isArray(data.excluded)) data.excluded = []; if (checked) { - data.excluded = data.excluded.filter(i => i !== idx); + meta.excluded = meta.excluded.filter(i => i !== idx); } else { - if (!data.excluded.includes(idx)) data.excluded.push(idx); + if (!meta.excluded.includes(idx)) meta.excluded.push(idx); } - widget.value = JSON.stringify(data); + metaWidget.value = JSON.stringify(meta); node.setDirtyCanvas?.(true, true); } } @@ -1489,14 +1469,14 @@ app.registerExtension({ manualWidget.serializeValue = () => manualWidget.value; } - let excludedWidget = this.widgets?.find((w) => w.name === "excluded_indices"); - if (!excludedWidget) { - excludedWidget = this.addWidget("text", "excluded_indices", "[]", () => {}); + let metaWidget = this.widgets?.find((w) => w.name === "viewer_meta"); + if (!metaWidget) { + metaWidget = this.addWidget("text", "viewer_meta", JSON.stringify({ lastInputHash: "", excluded: [] }), () => {}); } - if (excludedWidget) { - excludedWidget.type = "hidden"; - excludedWidget.computeSize = () => [0, -4]; - excludedWidget.serializeValue = () => excludedWidget.value; + if (metaWidget) { + metaWidget.type = "hidden"; + metaWidget.computeSize = () => [0, -4]; + metaWidget.serializeValue = () => metaWidget.value; } for (const w of this.widgets || []) { @@ -1569,8 +1549,6 @@ app.registerExtension({ const elements = STATE.nodeIdToElements.get(String(this.id)); if (elements) { elements.lastContentHash = ""; - elements.hasBackendContent = false; - elements.hasUserEdits = false; } const node = this; @@ -1609,10 +1587,18 @@ app.registerExtension({ } } } - const elements = STATE.nodeIdToElements.get(String(this.id)); - if (elements) { - elements.lastContentHash = ""; - } + const node = this; + setTimeout(() => { + try { + const elements = ensureElementsForNode(node); + if (elements) { + elements.lastContentHash = ""; + updateIframeContent(node, elements); + } + } catch (e) { + console.error("[WAS Viewer] onConfigure delayed update error:", e); + } + }, 100); } catch (e) { console.error("[WAS Viewer] onConfigure error:", e); } @@ -1623,20 +1609,46 @@ app.registerExtension({ nodeType.prototype.onExecuted = function (message) { const r = oldOnExecuted ? oldOnExecuted.apply(this, arguments) : undefined; try { - const msgStr = JSON.stringify(message); - console.log("[WAS Viewer] onExecuted message:", msgStr?.slice(0, 256)); - const newContent = message?.text?.[0]; - console.log("[WAS Viewer] newContent length:", newContent?.length, "preview:", newContent?.slice(0, 256)); - if (newContent !== undefined && newContent !== null && newContent !== "") { - setWidgetValue(this, "manual_content", String(newContent)); + const displayText = message?.text?.[0]; + const sourceContent = message?.source_content?.[0] || ""; + const sourceContentHash = message?.content_hash?.[0] || ""; + + const metaWidget = this.widgets?.find(w => w.name === "viewer_meta"); + let meta = { lastInputHash: "", excluded: [] }; + try { + meta = JSON.parse(metaWidget?.value || "{}"); + if (!meta.lastInputHash) meta.lastInputHash = ""; + if (!Array.isArray(meta.excluded)) meta.excluded = []; + } catch {} + + if (sourceContentHash && meta.lastInputHash === sourceContentHash) { + console.log("[WAS Viewer] onExecuted: source content hash unchanged (cached), skipping update"); + return r; + } + + if (sourceContent) { + console.log("[WAS Viewer] onExecuted: new source content, hash:", sourceContentHash, "preview:", sourceContent?.slice(0, 256)); + + meta.lastInputHash = sourceContentHash; + meta.excluded = []; + if (metaWidget) { + metaWidget.value = JSON.stringify(meta); + } + + setWidgetValue(this, "manual_content", sourceContent); + const elements = STATE.nodeIdToElements.get(String(this.id)); + if (elements) { + elements.lastContentHash = ""; + updateIframeContent(this, elements); + } + } else if (displayText !== undefined && displayText !== null && displayText !== "") { + console.log("[WAS Viewer] onExecuted: no source content, using display text"); + setWidgetValue(this, "manual_content", String(displayText)); const elements = STATE.nodeIdToElements.get(String(this.id)); if (elements) { - elements.hasBackendContent = true; elements.lastContentHash = ""; updateIframeContent(this, elements); } - } else { - console.log("[WAS Viewer] Skipping empty content"); } } catch (e) { console.error("[WAS Viewer] onExecuted error:", e);