diff --git a/docs/concepts/workspace/README.md b/docs/concepts/workspace/README.md index 2fb1358..161abf6 100644 --- a/docs/concepts/workspace/README.md +++ b/docs/concepts/workspace/README.md @@ -20,8 +20,8 @@ Diagnostics & logs. The in-app Design review contains the feature map below. One application shell replaces separate Admin, Gallery and Metadata shells. Use the approved shared ComfyAssets theme: ComfyUI dark/light, blue primary -actions, green positive roles, red negative roles and errors. Preserve all -existing controls and services while making their scope clearer. +actions, green positive roles, red negative roles and errors. Preserve existing +capabilities and services while consolidating controls and clarifying their scope. Browsing is image-led with readable thumbnails and one metadata inspector. Prompt library groups by positive; negative variants and runs share its gallery. @@ -62,7 +62,7 @@ multiple nodes or positive and negative encoder inputs. Verify Classic and Nodes | Metadata page / viewer sidebar / full-text popups | One reusable metadata inspector; inline complete prompt text | Consolidated | | Prompt add, edit, rating, notes, copy, delete | Recipe editor, shared details, contextual bulk actions; reference-safe archive | Retained, clarified | | Categories, tag rename / merge, autocomplete | Tags & categories; same vocabulary in every editor | Retained | -| Grid/list, thumbnail size, columns, pages/infinite scroll | One Appearance configuration; contextual layout / density overrides | Simplified | +| Grid/list, thumbnail size, columns, pages/infinite scroll | Infinite scroll only; one Appearance configuration and contextual layout / density overrides | Simplified | | Scan, rescan, monitoring directories, worker threads | Sources & scans; explicit metadata enrichment vs thumbnail work | Consolidated | | Ollama, HuggingFace, WD14 variants, thresholds, review | One Auto-tagging configuration and review flow | Retained | | Thumbnail quality, size limit, metadata cache, paths | Appearance + cache controls in Maintenance | Retained | @@ -118,3 +118,18 @@ instructions, generate sample suggestions, edit tags manually, and approve only the desired suggestions. Changes are staged until approval, apply to that image alone and do not resolve uncertain prompt associations. Changing models discards previous suggestions. Cancel preserves existing tags. All inference is simulated. + +## Continuous browsing decision + +The redesign uses infinite scroll only. Remove page numbers, previous/next page +buttons, paged/infinite mode selection and results-per-page controls. Keep request +batching internal. The mock appends 12 images at a time in Grid, List and Masonry; +it preserves existing card nodes, focus and selection while appending. It also +retains the loaded range when switching layouts or revisiting a search. A +keyboard-accessible Load more action provides an alternate trigger for the same +continuous stream, not pagination. Exhausted and empty results are explicit. + +Production will need bounded rendering, stable cursors, scroll restoration and +loading/error/retry behavior for large collections. Existing backend pagination +can continue providing internal request batches; no production tests or routes +are removed as part of this prototype. diff --git a/docs/concepts/workspace/app.js b/docs/concepts/workspace/app.js index ba01a32..eb85eee 100644 --- a/docs/concepts/workspace/app.js +++ b/docs/concepts/workspace/app.js @@ -5,6 +5,9 @@ import { mountTheme, } from "/toolkit/core.js"; const scope = createScope(); +const imageBatchSize = 12, + loadedImageCounts = new Map(); +let imageScrollObserver = null; let masonryObserver = null, masonryFrame = 0, tagSession = null; @@ -420,7 +423,7 @@ function renderImages() { button(icon("plus") + " New prompt", "new-prompt", "", "ca-primary"), ) + toolbar() + - `
${button(icon("grid") + " Grid", "image-view", 'data-value="grid" aria-pressed="' + (state.view === "grid") + '"')}${button(icon("list") + " List", "image-view", 'data-value="list" aria-pressed="' + (state.view === "list") + '"')}${button(icon("masonry") + " Masonry", "image-view", 'data-value="masonry" aria-pressed="' + (state.view === "masonry") + '"')}
${icon("folder")} All output folders${button("Group by positive prompt →", "go-library", "", "chip")}Illustrative previews
`; + `
${button(icon("grid") + " Grid", "image-view", 'data-value="grid" aria-pressed="' + (state.view === "grid") + '"')}${button(icon("list") + " List", "image-view", 'data-value="list" aria-pressed="' + (state.view === "list") + '"')}${button(icon("masonry") + " Masonry", "image-view", 'data-value="masonry" aria-pressed="' + (state.view === "masonry") + '"')}
${icon("folder")} All output folders${button("Group by positive prompt →", "go-library", "", "chip")}Illustrative previews
`; $("image-size").value = state.size; updateImages(); } @@ -430,12 +433,21 @@ function updateBulk() { : ""; } function updateImages() { + stopImageScroll(); stopMasonry(); - const shown = visibleImages(); + const matching = visibleImages(); + const key = imageQueryKey(); + const selectedIndex = matching.findIndex((i) => i.id === state.selectedImage); + const limit = Math.max( + loadedImageCounts.get(key) || imageBatchSize, + Math.ceil((selectedIndex + 1) / imageBatchSize) * imageBatchSize, + ); + loadedImageCounts.set(key, limit); + const shown = matching.slice(0, limit); if (shown.length && !shown.some((i) => i.id === state.selectedImage)) state.selectedImage = shown[0].id; $("result-count").textContent = - `${shown.length} images · ${new Set(shown.map((i) => i.prompt)).size} positive prompts`; + `${shown.length} of ${matching.length} images · ${new Set(matching.map((i) => i.prompt)).size} positive prompts`; $("image-grid").className = `image-grid ${state.size === "comfortable" ? "" : state.size} ${state.view === "list" ? "list-grid" : state.view === "masonry" ? "masonry-grid" : ""}`; $("image-grid").innerHTML = shown.length @@ -446,6 +458,68 @@ function updateImages() { : '

No image selected

Clear the filters or choose a matching image.

'; updateBulk(); if (state.view === "masonry" && shown.length) mountMasonry($("image-grid")); + updateImageContinuation(); +} +function imageQueryKey() { + return JSON.stringify([ + state.query, + state.category, + state.filter, + state.collection, + state.sort, + ]); +} +function stopImageScroll() { + imageScrollObserver?.disconnect(); + imageScrollObserver = null; +} +function updateImageContinuation() { + stopImageScroll(); + const continuation = $("gallery-continuation"); + if (!continuation) return; + const total = visibleImages().length, + loaded = $("image-grid").querySelectorAll(".media-card").length; + continuation.innerHTML = + loaded < total + ? `

${loaded} of ${total} images · more load as you scroll

${button("Load more images", "load-more-images")}` + : `

${total ? "You’re all caught up" : "No matching images"} · ${total} images

`; + if (loaded >= total) return; + const key = imageQueryKey(); + imageScrollObserver = new IntersectionObserver( + (entries) => { + if ( + entries.some((entry) => entry.isIntersecting) && + imageQueryKey() === key && + !document.querySelector("dialog[open]") + ) + loadMoreImages(); + }, + { rootMargin: "180px 0px" }, + ); + imageScrollObserver.observe(continuation); +} +function loadMoreImages(trigger = null) { + const grid = $("image-grid"); + if (!grid) return; + const loaded = grid.querySelectorAll(".media-card").length; + const matching = visibleImages(), + next = matching.slice(loaded, loaded + imageBatchSize); + if (!next.length) return; + stopImageScroll(); + stopMasonry(); + loadedImageCounts.set(imageQueryKey(), loaded + next.length); + grid.insertAdjacentHTML("beforeend", next.map(mediaCard).join("")); + $("result-count").textContent = + `${loaded + next.length} of ${matching.length} images · ${new Set(matching.map((i) => i.prompt)).size} positive prompts`; + if (state.view === "masonry") mountMasonry(grid); + updateImageContinuation(); + if (trigger?.dataset?.action === "load-more-images") + grid + .querySelector(`[data-action="open-image"][data-id="${next[0].id}"]`) + ?.focus(); + notify( + `${next.length} more images loaded. ${loaded + next.length} of ${matching.length}.`, + ); } function stopMasonry() { masonryObserver?.disconnect(); @@ -600,7 +674,7 @@ function renderSystem() { )}

Library health

${operation("3 ambiguous image links", "Review metadata before assigning a positive / negative pair.", "go-review", "Review metadata")}${operation("Backup status", "Last sample backup today at 09:30 · verification passed.", "system-tab", "View backups", 'data-value="backups"')}${operation("No missing thumbnails", "Generate only missing previews when necessary.", "job", "Review thumbnails", 'data-job="Thumbnail check"')}

Workspace preferences

${operation("One display configuration", "Grid, density and paging settings are shared across views.", "system-tab", "Appearance", 'data-value="appearance"')}${operation("2 sample output sources", "Monitoring and enrichment share one source list.", "system-tab", "Manage sources", 'data-value="sources"')}${operation("Statistics", "Usage by prompt, category, tag and date.", "stats", "Explore")}
`; break; case "appearance": - body = `

Browsing & display

One set of defaults; toolbar changes apply to the current view.

${setting("default-view", "Default layout", "Choose your starting image layout.", "select", ["Grid", "List", "Masonry"])}${setting("density", "Thumbnail size", "Readable previews by default; no fixed column-count setting.", "select", ["Comfortable", "Large", "Compact"])}${setting("loading", "Loading behavior", "One choice replaces separate page-size and infinite-scroll controls.", "select", ["Load more on scroll", "Paged results"])}${setting("batch-size", "Results per batch", "Controls request size, independent of thumbnail size.", "select", ["50", "100", "200"])}${setting("metadata-auto", "Load metadata on selection", "Use the same inspector in the gallery and image viewer.")}${setting("metadata-cache", "Cache metadata in this browser", "Reuse fetched metadata until it changes or you clear this cache.")}${setting("image-quality", "Image loading quality", "Preserve original / optimized / fast image-loading preferences.", "select", ["Optimized", "Original", "Fast"])}${setting("video-auto", "Autoplay videos", "Videos stay within the same gallery and use native playback controls.")}${setting("video-muted", "Mute videos by default", "Keep playback quiet while browsing.")}${setting("video-loop", "Loop videos", "Repeat a selected clip until you navigate away.")}${setting("paths", "Show file paths", "Paths appear under File & provenance, not across every card.")}${setting("open-mode", "Open from ComfyUI", "Keep the node pack’s popup / new-tab preference.", "select", ["New tab", "Popup window"])}${setting("timeout", "Node search auto-hide (seconds)", "Node search behavior only. Use 0 to keep results visible.", "number", [30])}
`; + body = `

Browsing & display

One set of defaults; toolbar changes apply to the current view.

${setting("default-view", "Default layout", "Choose your starting image layout.", "select", ["Grid", "List", "Masonry"])}${setting("density", "Thumbnail size", "Readable previews by default; no fixed column-count setting.", "select", ["Comfortable", "Large", "Compact"])}

Continuous browsing

Images load automatically as you scroll in Grid, List and Masonry. Batch sizing is handled internally.

Infinite scroll
${setting("metadata-auto", "Load metadata on selection", "Use the same inspector in the gallery and image viewer.")}${setting("metadata-cache", "Cache metadata in this browser", "Reuse fetched metadata until it changes or you clear this cache.")}${setting("image-quality", "Image loading quality", "Preserve original / optimized / fast image-loading preferences.", "select", ["Optimized", "Original", "Fast"])}${setting("video-auto", "Autoplay videos", "Videos stay within the same gallery and use native playback controls.")}${setting("video-muted", "Mute videos by default", "Keep playback quiet while browsing.")}${setting("video-loop", "Loop videos", "Repeat a selected clip until you navigate away.")}${setting("paths", "Show file paths", "Paths appear under File & provenance, not across every card.")}${setting("open-mode", "Open from ComfyUI", "Keep the node pack’s popup / new-tab preference.", "select", ["New tab", "Popup window"])}${setting("timeout", "Node search auto-hide (seconds)", "Node search behavior only. Use 0 to keep results visible.", "number", [30])}
`; break; case "sources": body = `${state.notice ? `

Bring older images into your prompt history

The schema upgrades automatically. An optional metadata scan links existing positives and negatives without regenerating all thumbnails.

${button("Later", "dismiss-notice")}${button("Review scan", "scan-review", "", "ca-primary")}
` : ""}

Image sources

${button("+ Add source", "add-source")}

The same folders power browsing, monitoring and metadata enrichment.

${operation("ComfyUI output", "/ComfyUI/output · watching · sample path", "scan-review", "Scan changes")}${operation("Archive", "/images/archive · manual scan · sample path", "scan-review", "Scan folder")}
${setting("monitoring", "Watch for new outputs", "New files enter the same library. Existing scans do not count as new runs.")}${setting("workers", "Worker threads", "Balance background scans and thumbnail work against generation.", "range", [4])}${operation("Enrich existing metadata", "Resolve both prompts, deduplicate recipes, preserve uncertain links.", "scan-review", "Review scan")}${operation("Thumbnail generation", "Generate missing previews; rebuilding all is a separate explicit option.", "thumbnail-review", "Review options")}
`; @@ -716,7 +790,7 @@ const featureMap = [ ], [ "Grid/list, thumbnail size, columns, pages/infinite scroll", - "One Appearance configuration; contextual layout / density overrides", + "Infinite scroll only; one Appearance configuration and contextual layout / density overrides", "Simplified", ], [ @@ -1094,6 +1168,7 @@ function navigate(event) { if (event) $("page-title")?.focus(); } function render() { + stopImageScroll(); stopMasonry(); nav(); const page = state.page; @@ -1123,6 +1198,7 @@ function render() { else renderImages(); } const actions = { + "load-more-images": loadMoreImages, "close-dialog": closeDialog, send: (b) => openSend( @@ -1654,6 +1730,9 @@ scope.listen(document, "change", (event) => { } }); scope.listen($("modal-close"), "click", closeDialog); +scope.listen($("modal"), "close", () => { + if ($("gallery-continuation")) updateImageContinuation(); +}); scope.listen($("viewer-close"), "click", () => $("viewer-dialog").close()); scope.listen($("viewer-dialog"), "close", () => { clearInterval(slideTimer); @@ -1687,6 +1766,7 @@ scope.listen($("theme"), "change", setTheme); scope.listen(matchMedia("(prefers-color-scheme: dark)"), "change", setTheme); scope.listen(window, "hashchange", navigate); scope.listen(window, "pagehide", () => { + stopImageScroll(); stopMasonry(); viewer?.destroy(); clearInterval(slideTimer); diff --git a/docs/concepts/workspace/screenshots/image-auto-tagging.png b/docs/concepts/workspace/screenshots/image-auto-tagging.png index 3f911b6..e3eabfb 100644 Binary files a/docs/concepts/workspace/screenshots/image-auto-tagging.png and b/docs/concepts/workspace/screenshots/image-auto-tagging.png differ diff --git a/docs/concepts/workspace/screenshots/images-dark.png b/docs/concepts/workspace/screenshots/images-dark.png index b904e6c..546b10a 100644 Binary files a/docs/concepts/workspace/screenshots/images-dark.png and b/docs/concepts/workspace/screenshots/images-dark.png differ diff --git a/docs/concepts/workspace/screenshots/images-light.png b/docs/concepts/workspace/screenshots/images-light.png index ad8aa81..725734e 100644 Binary files a/docs/concepts/workspace/screenshots/images-light.png and b/docs/concepts/workspace/screenshots/images-light.png differ diff --git a/docs/concepts/workspace/screenshots/images-masonry.png b/docs/concepts/workspace/screenshots/images-masonry.png index 99b4f83..8e9dda6 100644 Binary files a/docs/concepts/workspace/screenshots/images-masonry.png and b/docs/concepts/workspace/screenshots/images-masonry.png differ diff --git a/docs/concepts/workspace/screenshots/images-scroll-loaded.png b/docs/concepts/workspace/screenshots/images-scroll-loaded.png new file mode 100644 index 0000000..66771cb Binary files /dev/null and b/docs/concepts/workspace/screenshots/images-scroll-loaded.png differ diff --git a/docs/concepts/workspace/screenshots/mobile.png b/docs/concepts/workspace/screenshots/mobile.png index c61909d..02636a4 100644 Binary files a/docs/concepts/workspace/screenshots/mobile.png and b/docs/concepts/workspace/screenshots/mobile.png differ diff --git a/docs/concepts/workspace/screenshots/settings.png b/docs/concepts/workspace/screenshots/settings.png index 40e288b..52a2c27 100644 Binary files a/docs/concepts/workspace/screenshots/settings.png and b/docs/concepts/workspace/screenshots/settings.png differ diff --git a/docs/concepts/workspace/style.css b/docs/concepts/workspace/style.css index 199d70e..db4b3c4 100644 --- a/docs/concepts/workspace/style.css +++ b/docs/concepts/workspace/style.css @@ -1692,3 +1692,12 @@ progress { border-top: 1px solid var(--line); z-index: 1; } + +.infinite-continuation { + padding-block: 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-block-size: 72px; +}