diff --git a/.agents/logs/ai_changelog.md b/.agents/logs/ai_changelog.md index 47bc44a..8310d82 100644 --- a/.agents/logs/ai_changelog.md +++ b/.agents/logs/ai_changelog.md @@ -1,5 +1,20 @@ # AI Changelog +## [Snapshot] 2026-08-06 - Parameter Notebook UI Redesign & Hero Gallery + +**Implemented** + +- **Parameter Notebook Layout**: Completely redesigned the parameter notebook to use a modern Dashboard layout. The parameter summary now uses a spacious CSS Grid format (`.anomalous-recipe-detail-summary-grid`). +- **Node Cards & Smart Expansion**: Each node's parameters are wrapped in a styled card (`.anomalous-recipe-detail-parameter-node`). The inner widgets use a responsive grid layout to eliminate vertical cramming. Overly long nodes automatically collapse to a maximum height (`is-collapsed`) with a smooth gradient mask and a bottom expand button. +- **Hero Image Gallery (Plan B)**: Replaced the bottom inline gallery section with a prominent Hero Section (`.anomalous-recipe-detail-hero`) at the top. The first generated image acts as a large cover banner. +- **Modal Gallery Viewer**: If a recipe has multiple generated images, a sleek glassmorphism button overlay appears on the hero image (e.g., `🖼️ 图库 (5)`). Clicking it opens a native HTML `` containing the full gallery grid, preventing the parameter view from bloating vertically while keeping the core browsing experience focused. + +**Validation** + +- Node widget grid correctly scales based on available width. +- Smart expansion accurately toggles `max-height` via CSS classes without recalculating DOM dimensions. +- Native `` modal manages z-index and backdrop natively, avoiding clashes with the main UI shell. + ## [Snapshot] 2026-08-06 - Parameter Notebook UI Tweaks **Implemented** diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js index 08f244e..bd240e1 100644 --- a/web/modules/ui_recipe_detail.js +++ b/web/modules/ui_recipe_detail.js @@ -1419,7 +1419,12 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s : workflowNode.widgets_values.map((value, index) => ({ name: `${t('recipeDetailWidget')} ${index + 1}`, index, value })); const block = document.createElement('article'); block.className = 'anomalous-recipe-detail-parameter-node'; - appendText(block, 'strong', [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode')); + const titleText = [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode'); + appendText(block, 'strong', titleText, 'anomalous-recipe-detail-node-title'); + + const widgetsContainer = document.createElement('div'); + widgetsContainer.className = 'anomalous-recipe-detail-node-widgets'; + for (let visibleIndex = 0; visibleIndex < widgets.length && rendered < 1200; visibleIndex += 1) { const widget = widgets[visibleIndex] || {}; const index = Number.isInteger(widget.index) ? widget.index : visibleIndex; @@ -1427,7 +1432,7 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s const value = workflowNode.widgets_values[index]; const row = document.createElement('label'); row.className = 'anomalous-recipe-detail-parameter-row anomalous-recipe-parameter-editor-row'; - appendText(row, 'span', widget.name || `${t('recipeDetailWidget')} ${index + 1}`, 'anomalous-recipe-detail-label'); + appendText(row, 'span', widget.name || `${t('recipeDetailWidget')} ${index + 1}`, 'anomalous-recipe-detail-parameter-name'); const volatile = isVolatileParameter(node, widget, index); const sensitive = /(?:api.?key|access.?token|auth|password|passwd|secret|credential)/i.test(String(widget.name || '')); if (volatile || sensitive) { @@ -1450,14 +1455,32 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s input.classList.remove('is-invalid'); } catch (error) { input.classList.add('is-invalid'); + console.warn('Parameter input invalid:', error); } }; row.appendChild(input); } - block.appendChild(row); + widgetsContainer.appendChild(row); rendered += 1; } - if (block.querySelector('.anomalous-recipe-parameter-editor-row')) nodeList.appendChild(block); + if (widgetsContainer.childElementCount) { + block.appendChild(widgetsContainer); + if (widgetsContainer.childElementCount > 4) { + block.classList.add('is-collapsed'); + const expandOverlay = document.createElement('div'); + expandOverlay.className = 'anomalous-recipe-detail-node-expand'; + const expandBtn = document.createElement('button'); + expandBtn.className = 'anomalous-btn-ghost'; + expandBtn.innerHTML = '▼'; + expandBtn.onclick = () => { + block.classList.remove('is-collapsed'); + expandOverlay.remove(); + }; + expandOverlay.appendChild(expandBtn); + block.appendChild(expandOverlay); + } + nodeList.appendChild(block); + } } if (!nodeList.childElementCount) appendText(nodeList, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted'); editor.appendChild(nodeList); @@ -1703,7 +1726,9 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, const block = document.createElement('article'); block.className = 'anomalous-recipe-detail-parameter-node'; const title = [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode'); - appendText(block, 'strong', title); + appendText(block, 'strong', title, 'anomalous-recipe-detail-node-title'); + const widgetsContainer = document.createElement('div'); + widgetsContainer.className = 'anomalous-recipe-detail-node-widgets'; for (let visibleIndex = 0; visibleIndex < widgets.length && renderedWidgets < 1200; visibleIndex += 1) { const widget = widgets[visibleIndex] || {}; const index = Number.isInteger(widget.index) ? widget.index : visibleIndex; @@ -1712,59 +1737,97 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, : widget.value; const label = widget.name || `${t('recipeDetailWidget')} ${index + 1}`; const volatile = isVolatileParameter(node, widget, index); - if (renderParameterField(block, label, volatile ? 0 : value, { + if (renderParameterField(widgetsContainer, label, volatile ? 0 : value, { redact: volatile, copy: false, collapse: false, })) renderedWidgets += 1; } - if (block.querySelector('.anomalous-recipe-detail-parameter-row')) nodeList.appendChild(block); + if (widgetsContainer.childElementCount) { + block.appendChild(widgetsContainer); + if (widgetsContainer.childElementCount > 4) { + block.classList.add('is-collapsed'); + const expandOverlay = document.createElement('div'); + expandOverlay.className = 'anomalous-recipe-detail-node-expand'; + const expandBtn = document.createElement('button'); + expandBtn.className = 'anomalous-btn-ghost'; + expandBtn.innerHTML = '▼'; + expandBtn.onclick = () => { + block.classList.remove('is-collapsed'); + expandOverlay.remove(); + }; + expandOverlay.appendChild(expandBtn); + block.appendChild(expandOverlay); + } + nodeList.appendChild(block); + } } if (nodeList.childElementCount) nodesSection.appendChild(nodeList); else appendText(nodesSection, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted'); wrapper.append(intro, summary, nodesSection); - - const gallerySection = document.createElement('section'); - gallerySection.className = 'anomalous-recipe-detail-section anomalous-recipe-gallery'; - const heading = document.createElement('div'); - heading.className = 'anomalous-recipe-detail-section-heading'; - appendText(heading, 'h5', t('recipeParameterGallery')); - const refreshButton = button(heading, t('recipeGalleryRefresh'), 'anomalous-btn-ghost anomalous-recipe-gallery-refresh'); - refreshButton.onclick = () => { void refreshGallery(true); }; - gallerySection.appendChild(heading); - if (gallery.status === 'loading') { - appendText(gallerySection, 'p', t('recipeParameterGalleryLoading'), 'anomalous-recipe-detail-muted'); - } else if (gallery.status === 'error') { - appendText(gallerySection, 'p', t('recipeParameterGalleryError'), 'anomalous-recipe-detail-muted'); - } else if (gallery.status === 'ready' && gallery.images.length) { - appendText(gallerySection, 'small', t('recipeGalleryScanHint').replace('{count}', String(gallery.scanned || 0)), 'anomalous-recipe-detail-muted'); - const grid = document.createElement('div'); - grid.className = 'anomalous-recipe-gallery-grid'; - for (const sourceImage of gallery.images) { - const card = document.createElement('article'); - card.className = 'anomalous-recipe-gallery-card'; - const url = outputImageUrl(sourceImage); - const image = document.createElement('img'); - image.src = url; - image.alt = t('recipeGalleryOpenImage'); - image.loading = 'lazy'; - image.onclick = () => { void showGalleryComparison(card, owner, sourceImage); }; - card.appendChild(image); - const actions = document.createElement('div'); - actions.className = 'anomalous-recipe-gallery-card-actions'; - const viewImage = button(actions, t('recipeGalleryOpenImage'), 'anomalous-btn-ghost'); - viewImage.onclick = () => owner.showGalleryViewer?.(url); - const compare = button(actions, t('recipeGalleryCompare'), 'anomalous-btn-ghost'); - compare.onclick = () => { void showGalleryComparison(card, owner, sourceImage); }; - card.appendChild(actions); - grid.appendChild(card); + + // Replace old bottom gallery with Hero Section at the top + if (gallery.status === 'ready' && gallery.images.length) { + const heroSection = document.createElement('section'); + heroSection.className = 'anomalous-recipe-detail-hero'; + + const heroImage = document.createElement('img'); + heroImage.src = outputImageUrl(gallery.images[0]); + heroImage.className = 'anomalous-recipe-detail-hero-image'; + heroImage.onclick = () => owner.showGalleryViewer?.(heroImage.src); + heroSection.appendChild(heroImage); + + if (gallery.images.length > 1) { + const galleryButton = document.createElement('button'); + galleryButton.className = 'anomalous-recipe-detail-hero-gallery-btn'; + galleryButton.innerHTML = `🖼️ ${t('recipeParameterGallery')} (${gallery.images.length})`; + galleryButton.onclick = () => { + const dialog = document.createElement('dialog'); + dialog.className = 'anomalous-recipe-gallery-dialog'; + + const closeBtn = document.createElement('button'); + closeBtn.className = 'anomalous-dialog-close anomalous-btn-ghost'; + closeBtn.innerHTML = '✕'; + closeBtn.onclick = () => dialog.close(); + dialog.appendChild(closeBtn); + + const heading = document.createElement('h3'); + heading.textContent = t('recipeParameterGallery'); + heading.className = 'anomalous-recipe-gallery-dialog-title'; + dialog.appendChild(heading); + + const grid = document.createElement('div'); + grid.className = 'anomalous-recipe-gallery-grid'; + for (const sourceImage of gallery.images) { + const card = document.createElement('article'); + card.className = 'anomalous-recipe-gallery-card'; + const url = outputImageUrl(sourceImage); + const image = document.createElement('img'); + image.src = url; + image.loading = 'lazy'; + image.onclick = () => { void showGalleryComparison(card, owner, sourceImage); }; + card.appendChild(image); + const actions = document.createElement('div'); + actions.className = 'anomalous-recipe-gallery-card-actions'; + const viewBtn = button(actions, t('recipeGalleryOpenImage'), 'anomalous-btn-ghost'); + viewBtn.onclick = () => owner.showGalleryViewer?.(url); + const compareBtn = button(actions, t('recipeGalleryCompare'), 'anomalous-btn-ghost'); + compareBtn.onclick = () => { void showGalleryComparison(card, owner, sourceImage); }; + card.appendChild(actions); + grid.appendChild(card); + } + dialog.appendChild(grid); + document.body.appendChild(dialog); + + dialog.addEventListener('close', () => dialog.remove()); + dialog.showModal(); + }; + heroSection.appendChild(galleryButton); } - gallerySection.appendChild(grid); - } else if (gallery.status === 'ready') { - appendText(gallerySection, 'p', t('recipeParameterGalleryEmpty'), 'anomalous-recipe-detail-muted'); + wrapper.insertBefore(heroSection, intro); } - wrapper.appendChild(gallerySection); + layout.append(sidebar, wrapper); content.appendChild(layout); } diff --git a/web/styles.css b/web/styles.css index 7741d9f..14361bc 100644 --- a/web/styles.css +++ b/web/styles.css @@ -2961,3 +2961,169 @@ .anomalous-recipe-parameter-notebook-item.is-switching { animation: anomalous-parameter-switch 0.25s cubic-bezier(0.1, 0.9, 0.2, 1) forwards; } + +/* Parameter Summary Grid Styling */ +.anomalous-recipe-detail-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + margin-bottom: 24px; +} + +.anomalous-recipe-detail-summary-grid .anomalous-recipe-detail-parameter-row.is-wide { + grid-column: 1 / -1; +} + +/* Node Parameter Card Styling */ +.anomalous-recipe-detail-parameter-node { + position: relative; + padding: 16px; + margin-bottom: 16px; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + transition: max-height 0.3s ease; +} + +.anomalous-recipe-detail-node-title { + display: block; + margin-bottom: 12px; + font-size: 1.05rem; + color: #e0e0e0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-bottom: 8px; +} + +/* Grid Layout for Node Widgets */ +.anomalous-recipe-detail-node-widgets { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +/* Smart Expansion (Collapsible Nodes) */ +.anomalous-recipe-detail-parameter-node.is-collapsed { + max-height: 180px; + overflow: hidden; +} + +.anomalous-recipe-detail-node-expand { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 70px; + background: linear-gradient(to bottom, transparent 0%, #202020 80%, #202020 100%); + display: flex; + align-items: flex-end; + justify-content: center; + padding-bottom: 8px; + z-index: 10; + border-radius: 0 0 8px 8px; +} + +.anomalous-recipe-detail-node-expand button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + padding: 4px 16px; + font-size: 0.8rem; + color: #ccc; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.anomalous-recipe-detail-node-expand button:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +/* Hero Section Styling */ +.anomalous-recipe-detail-hero { + position: relative; + width: 100%; + max-height: 350px; + margin-bottom: 24px; + border-radius: 8px; + overflow: hidden; + background: rgba(0, 0, 0, 0.2); +} + +.anomalous-recipe-detail-hero-image { + width: 100%; + height: 100%; + max-height: 350px; + object-fit: cover; + display: block; + cursor: pointer; + transition: transform 0.3s ease; +} + +.anomalous-recipe-detail-hero-image:hover { + transform: scale(1.02); +} + +.anomalous-recipe-detail-hero-gallery-btn { + position: absolute; + bottom: 16px; + right: 16px; + background: rgba(0, 0, 0, 0.6); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 8px 16px; + border-radius: 20px; + font-size: 0.9rem; + cursor: pointer; + backdrop-filter: blur(8px); + transition: background 0.2s, transform 0.2s; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.anomalous-recipe-detail-hero-gallery-btn:hover { + background: rgba(0, 0, 0, 0.8); + transform: translateY(-2px); +} + +/* Modal Gallery Dialog Styling */ +.anomalous-recipe-gallery-dialog { + background: #1e1e1e; + color: #eee; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 24px; + width: 90vw; + max-width: 1200px; + max-height: 90vh; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + overflow-y: auto; +} + +.anomalous-recipe-gallery-dialog::backdrop { + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); +} + +.anomalous-recipe-gallery-dialog-title { + margin-top: 0; + margin-bottom: 16px; + font-size: 1.4rem; + font-weight: 600; +} + +.anomalous-dialog-close { + position: absolute; + top: 16px; + right: 16px; + font-size: 1.2rem; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + cursor: pointer; +}