diff --git a/pyproject.toml b/pyproject.toml index 8cd95df..f8cd3c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI_Viewer" -version = "0.4.0" +version = "0.4.1" description = "Extensible content viewer for ComfyUI: HTML, Markdown (LaTeX, Mermaid, etc), Code, Images, and more" readme = "README.md" requires-python = ">=3.10" diff --git a/web/comfy_viewer.js b/web/comfy_viewer.js index 9f03bb4..d1e2806 100644 --- a/web/comfy_viewer.js +++ b/web/comfy_viewer.js @@ -608,7 +608,7 @@ function ensureElementsForNode(node) { textarea: null, typeLabel: null, toggleAllBtn: null, - lastContentHash: "", + lastContentHash: null, isEditing: false, listEditContainer: null, listTextareas: null, @@ -896,20 +896,26 @@ function updateIframeContent(node, elements, forceView = null) { if (elements.isEditing) return; const content = getNodeContent(node, elements); - const contentHash = content ? content.length + "_" + content.slice(0, 100) : ""; + const contentHash = content ? content.length + "_" + content.slice(0, 100) : "__empty__"; + + // Check for manual view selection from the toolbar dropdown + let manualViewType = null; + if (!content && elements.viewSelector && elements.viewSelector.value) { + manualViewType = elements.viewSelector.value; + } // When content is empty but the upstream node is a UI view provider (e.g. // OpenReel), load the view in standalone mode so the user can work in it // before the workflow has been executed. We use a special hash so this // only runs once (not every frame). let upstreamViewType = null; - if (!content) { + if (!content && !manualViewType) { const upstreamClass = getUpstreamNodeClass(node); if (upstreamClass && upstreamClass.includes("OpenReel")) { upstreamViewType = "openreel_video"; } } - const effectiveHash = upstreamViewType ? ("_upstream:" + upstreamViewType) : contentHash; + const effectiveHash = (manualViewType || upstreamViewType) ? ("_manual:" + (manualViewType || upstreamViewType)) : contentHash; if (effectiveHash === elements.lastContentHash && !forceView) return; elements.lastContentHash = effectiveHash; @@ -938,11 +944,19 @@ function updateIframeContent(node, elements, forceView = null) { contentType = detectContentType(content); } } else { - contentType = upstreamViewType || detectContentType(content); - elements.currentView = null; + // Manual view_type only applies when there's no content (standalone mode) + // When content exists, always use normal detection + if (!content && manualViewType) { + contentType = manualViewType; + } else { + contentType = upstreamViewType || detectContentType(content); + } + elements.currentView = manualViewType || null; elements.multiviewContent = null; + + // Update view selector for empty content (shows all views) if (elements.viewSelector) { - elements.viewSelector.style.display = "none"; + updateViewSelector(elements.viewSelector, content, manualViewType); } } @@ -984,10 +998,10 @@ function updateIframeContent(node, elements, forceView = null) { let html; const nodeId = String(node.id); - if (!displayContent) { + if (!displayContent && !manualViewType) { html = buildIframeContent("

No content. Click Edit to add content or connect a STRING input.

", "html", theme, [], nodeId); } else { - html = buildIframeContent(finalContent, contentType, theme, excluded, nodeId); + html = buildIframeContent(finalContent || '', contentType, theme, excluded, nodeId); } // Check if the view provides a direct URL (e.g. OpenReel app served by its own endpoint). @@ -999,7 +1013,7 @@ function updateIframeContent(node, elements, forceView = null) { diag(`updateIframeContent: directUrl=${!!directUrl} directUrlLoaded=${!!elements.directUrlLoaded} nodeId=${String(node.id)} hasContent=${!!displayContent}`); // For OpenReel: defer loading until we have actual content to avoid hard refresh race conditions - if (directUrl && !displayContent && !elements.directUrlLoaded) { + if (directUrl && !displayContent && !elements.directUrlLoaded && !manualViewType) { diag(`updateIframeContent: OpenReel deferred - no content yet`); // Show placeholder, don't load the app yet const placeholderHtml = buildIframeContent("

Waiting for video content...

", "html", theme, [], nodeId); @@ -1022,9 +1036,9 @@ function updateIframeContent(node, elements, forceView = null) { return; } - const needsBlobUrl = !directUrl && displayContent && ( + const needsBlobUrl = !directUrl && (displayContent || manualViewType) && ( viewNeedsBlobUrl(contentType) || - (contentType === "html" && ( + (contentType === "html" && displayContent && ( displayContent.includes("WebAssembly") || displayContent.includes("wasm") || displayContent.includes("createUnityInstance") || @@ -1033,7 +1047,7 @@ function updateIframeContent(node, elements, forceView = null) { )) ); - const scriptData = displayContent ? getViewScriptData(contentType, finalContent) : []; + const scriptData = (displayContent || manualViewType) ? getViewScriptData(contentType, finalContent || '') : []; // Mark that a directUrl load has been queued. if (directUrl) { @@ -1053,9 +1067,15 @@ function updateIframeContent(node, elements, forceView = null) { * @param {string} viewName - Name of view to switch to */ function handleViewChange(node, elements, viewName) { - if (!elements.multiviewContent) return; + if (elements.multiviewContent) { + elements.lastContentHash = ""; + updateIframeContent(node, elements, viewName); + return; + } + // Manual view selection on empty nodes elements.lastContentHash = ""; - updateIframeContent(node, elements, viewName); + elements.directUrlLoaded = false; + updateIframeContent(node, elements); } window.addEventListener("message", (event) => { diff --git a/web/controls/controls_bar.js b/web/controls/controls_bar.js index dbbb2aa..0413b28 100644 --- a/web/controls/controls_bar.js +++ b/web/controls/controls_bar.js @@ -5,7 +5,7 @@ import { computeThemeTokens } from "../utils/theme.js"; import { createZipBlob } from "../utils/zip.js"; -import { detectContentType, isMultiviewContent, getMultiviewOptions, parseMultiviewContent, stripContentMarker } from "../views/view_loader.js"; +import { detectContentType, isMultiviewContent, getMultiviewOptions, parseMultiviewContent, stripContentMarker, getAllViews } from "../views/view_loader.js"; const CONTROLS_HEIGHT = 32; const LIST_SEPARATOR = "\n---LIST_SEPARATOR---\n"; @@ -80,35 +80,77 @@ function createViewSelector(theme, node, elements, callbacks) { * @param {string} content - Content to check for multi-view * @param {string} currentView - Currently selected view name */ -export function updateViewSelector(selector, content, currentView) { +export async function updateViewSelector(selector, content, currentView) { if (!selector) return; - if (!isMultiviewContent(content)) { - selector.style.display = "none"; - selector.innerHTML = ""; - return; - } - - const options = getMultiviewOptions(content); - if (options.length <= 1) { - selector.style.display = "none"; - selector.innerHTML = ""; - return; - } - - // Build options - selector.innerHTML = ""; - for (const opt of options) { - const option = document.createElement("option"); - option.value = opt.name; - option.textContent = opt.displayName; - if (opt.name === currentView) { - option.selected = true; + // For multi-view content, show the multi-view selector + if (isMultiviewContent(content)) { + const options = getMultiviewOptions(content); + if (options.length <= 1) { + selector.style.display = "none"; + selector.innerHTML = ""; + return; } - selector.appendChild(option); + + // Build multi-view options + selector.innerHTML = ""; + for (const opt of options) { + const option = document.createElement("option"); + option.value = opt.name; + option.textContent = opt.displayName; + if (opt.name === currentView) { + option.selected = true; + } + selector.appendChild(option); + } + + selector.style.display = "inline-block"; + return; } - selector.style.display = "inline-block"; + // For empty content, show all available views for manual selection + if (!content || content.trim() === "") { + const { loadAllViews } = await import("../views/view_loader.js"); + await loadAllViews(); + + const allViews = getAllViews(); + + if (allViews.size === 0) { + selector.style.display = "none"; + selector.innerHTML = ""; + return; + } + + // Build options from all registered views + selector.innerHTML = ""; + + // Add "Auto" option as default + const autoOption = document.createElement("option"); + autoOption.value = ""; + autoOption.textContent = "Auto"; + if (!currentView) { + autoOption.selected = true; + } + selector.appendChild(autoOption); + + // Add all registered views + for (const [id, view] of allViews) { + const option = document.createElement("option"); + option.value = id; + option.textContent = view.displayName || id; + if (id === currentView) { + option.selected = true; + } + selector.appendChild(option); + } + + selector.style.display = "inline-block"; + return; + } + + // For normal content with a view, hide the selector + selector.style.display = "none"; + selector.innerHTML = ""; } /**