diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1a1af7a..38a3850 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -153,7 +153,7 @@ DOM or live LiteGraph state. features compact vertical spacing with an inline topbar hosting the preset name input (`.anomalous-prompt-name-input`), direct `[💾 保存方案]` button, secondary action dropdown (`[··· 更多]` for new draft, preset tags, and JSON export), and window close button (`[✕]`), completely eliminating bulky standalone metadata strips; in docked sidebar mode (`#anomalous-container.anomalous-docked`) and narrow screen layouts (<860px), adopts a Unified Dual-Zone Flow (上下联动一体流): completely eliminates clumsy tab switching (`[词卡库 | 拼装台 | 全部]`), arranging the Assembler Stage on top (`order: 1`, max-height 54%) and Candidate Cards Deck on bottom (`order: 2`, max-height 50%), both with independent scroll containers to ensure candidate cards and assembled Lego blocks remain simultaneously visible; the Cards Deck hosts candidate prompt cards in a compact 2-column chip grid (`repeat(auto-fill, minmax(130px, 1fr))`) with 1-tap instant assembly (`cardEl.onclick` adds card to the active track with `@keyframes anomalous-just-added` animated feedback) alongside native HTML5 drag-and-drop, - features restrained dark slate backgrounds (`#24262E`) with category-specific left accent borders (Base/Style/Subject/Trigger), + features tactile glassmorphic card styling (dual-layer elevation shadows, top edge highlight, and category glow accents), a physical drag grip (`⠿`), a usage tip banner (`💡 点击词卡或拖拽至右侧拼装台`), monospace prompt snippet chips, and high-visibility `[+ 加入拼装台]` action buttons with instant click feedback, minimalist empty state container (`.anomalous-source-empty`), a streamlined col-header holding a unified action button group (`[🎯 提取]`, `[📥 导入]`, `[➕ 新建]`), one-click canvas node prompt extraction (`🎯 提取`, reading selected ComfyUI text nodes like `CLIPTextEncode` with downstream link connection traversal for accurate negative conditioning detection, auto-generating categorised cards) and one-click Material Library batch sync (`📥 导入`, cancellable fetch scoped to `category=prompts`), plus on-demand persistent card creation @@ -162,6 +162,7 @@ DOM or live LiteGraph state. the Assembler Stage hosts the sequential Lego block track (`anomalous-assembly-track`), compact modular blocks (~52px height) allowing generous top-to-bottom sequence stacking, supports bidirectional drag-and-drop reordering with ghost indicator lines, role-aware up/down swapping, instant A/B bypass toggles (greyscale dimming without deleting), interactive category pills, one-click Smart Sort (`🪄 按分类排序`, Base ➔ Style ➔ Subject ➔ Trigger), + an interactive Auto-snap Dock (`.anomalous-assembly-snap-dock`) positioned at the bottom of the track that magnetically detects dragover across the empty lower track area to auto-snap dragged cards or blocks to the end with animated visual guidance, and a permanently visible sticky Action Dock (`.anomalous-prompt-action-dock`) at the bottom of the assembler stage: displays live synthesis statistics (`✨ X字 · Y块`), quick copy button (`[📋]`), expandable compiled text preview drawer (`[📄 最终文本 ▾]`), insertion position selector (`[在后 ▾] / [在前 ▾]`), and the ComfyUI canvas node direct injection bar (`[#ID 节点名称] [⬇️ 写入正向/负向]`), ensuring canvas node injection is always 1-click away and never buried inside collapsed drawers; `prompt_composition.js` provides bidirectional schema mapping (`planToWorkbenchDraft` and `workbenchDraftToSavedPlan`) ensuring 100% roundtrip data integrity across `anomalous-prompt-plan-v1` and `version: 2`, lossless legacy dual-role splitting and trailing text retention, diff --git a/web/modules/ui_prompt_composer.js b/web/modules/ui_prompt_composer.js index a8dd061..13e1d3c 100644 --- a/web/modules/ui_prompt_composer.js +++ b/web/modules/ui_prompt_composer.js @@ -444,6 +444,15 @@ function buildPromptComposer(owner, container, options = {}) { const newCardForm = text(leftPanel, 'div', '', 'anomalous-workbench-new-card-form'); newCardForm.style.display = 'none'; + // Usage Tip Banner + const tipBanner = text(leftPanel, 'div', '', 'anomalous-workbench-tip-banner'); + tipBanner.innerHTML = ` + + + + ${window.anomalous_browser_lang === 'zh' ? '点击词卡或拖拽至右侧拼装台,自由调换顺序' : 'Click card or drag to right stage to assemble'} + `; + // Source Cards List const sourceCardsList = text(leftPanel, 'div', '', 'anomalous-source-cards-list'); @@ -474,6 +483,47 @@ function buildPromptComposer(owner, container, options = {}) { // Blocks Container & Dropzone const blocksContainer = text(rightPanel, 'div', '', 'anomalous-mixer-blocks-container anomalous-assembly-track'); + blocksContainer.ondragover = (e) => { + e.preventDefault(); + const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock'); + if (snapDock && (e.target === blocksContainer || e.target.closest('.anomalous-assembly-snap-dock'))) { + snapDock.classList.add('is-drag-over'); + blocksContainer.classList.add('is-drag-over-end'); + } + }; + blocksContainer.ondragleave = (e) => { + if (!blocksContainer.contains(e.relatedTarget)) { + const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock'); + snapDock?.classList.remove('is-drag-over'); + blocksContainer.classList.remove('is-drag-over-end'); + } + }; + blocksContainer.ondrop = (e) => { + if (e.target === blocksContainer || e.target.closest('.anomalous-assembly-snap-dock')) { + e.preventDefault(); + const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock'); + snapDock?.classList.remove('is-drag-over'); + blocksContainer.classList.remove('is-drag-over-end'); + const jsonStr = e.dataTransfer.getData('application/json'); + if (jsonStr) { + try { + addSourceCardToMixer(JSON.parse(jsonStr)); + return; + } catch (err) {} + } + if (draggedBlockId) { + const fromIndex = draft.plan.parts.findIndex(p => p.id === draggedBlockId); + if (fromIndex >= 0) { + const [moved] = draft.plan.parts.splice(fromIndex, 1); + draft.plan.parts.push(moved); + syncDraftSynthesizedText(draft); + renderBlocksList(); + updateOutputPreview(); + } + } + } + }; + // Bottom Live Assembled Action Dock (Always visible sticky bar at bottom of assembler) const outputDeck = text(rightPanel, 'div', '', 'anomalous-mixer-deck-output anomalous-prompt-action-dock is-collapsed'); @@ -938,13 +988,7 @@ function buildPromptComposer(owner, container, options = {}) { filtered.forEach(card => { const cardEl = text(sourceCardsList, 'article', '', `anomalous-source-prompt-card is-cat-${card.category}`); cardEl.setAttribute('draggable', 'true'); - cardEl.title = window.anomalous_browser_lang === 'zh' ? '点击直接加入上方拼装台(也可拖拽)' : 'Click to add to mixer above (or drag)'; - - // Instant tap to add card to mixer - cardEl.onclick = (e) => { - if (e.target.closest('button') || e.target.closest('input')) return; - addSourceCardToMixer(card); - }; + cardEl.title = window.anomalous_browser_lang === 'zh' ? '点击直接加入右侧拼装台(也可按住拖拽)' : 'Click to add to stage (or drag)'; // Drag Start -> transfer prompt card payload cardEl.ondragstart = (e) => { @@ -970,6 +1014,9 @@ function buildPromptComposer(owner, container, options = {}) { const header = text(cardEl, 'div', '', 'anomalous-source-card-header'); const meta = CATEGORY_META[card.category] || CATEGORY_META.subject; + const grip = text(header, 'span', '⠿', 'anomalous-source-card-grip'); + grip.title = window.anomalous_browser_lang === 'zh' ? '按住拖拽至拼装台' : 'Drag to assemble'; + const badge = text(header, 'span', window.anomalous_browser_lang === 'zh' ? meta.zh : meta.en, 'anomalous-source-card-badge'); badge.style.color = meta.color; badge.style.backgroundColor = meta.bg; @@ -994,6 +1041,9 @@ function buildPromptComposer(owner, container, options = {}) { // Card Footer Actions const footer = text(cardEl, 'div', '', 'anomalous-source-card-footer'); + const hint = text(footer, 'span', '', 'anomalous-source-card-drag-hint'); + hint.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '点击或拖拽' : 'Click or Drag'}`; + const actions = text(footer, 'div', '', 'anomalous-source-card-actions'); actions.style.marginLeft = 'auto'; @@ -1021,11 +1071,32 @@ function buildPromptComposer(owner, container, options = {}) { } const dockBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-dock'); - dockBtn.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '加入组合' : 'Add to Plan'}`; - dockBtn.title = window.anomalous_browser_lang === 'zh' ? '加入当前拼装组合' : 'Add into mixer track'; + dockBtn.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '加入拼装台' : 'Add to Stage'}`; + dockBtn.title = window.anomalous_browser_lang === 'zh' ? '点击加入当前拼装台(也可直接点击卡片)' : 'Add into mixer track'; + + const handleCardAdd = () => { + cardEl.classList.add('is-clicked'); + setTimeout(() => cardEl.classList.remove('is-clicked'), 500); + const originalHtml = dockBtn.innerHTML; + dockBtn.innerHTML = `✓ ${window.anomalous_browser_lang === 'zh' ? '已加入' : 'Added'}`; + dockBtn.classList.add('is-success'); + setTimeout(() => { + if (dockBtn.isConnected) { + dockBtn.innerHTML = originalHtml; + dockBtn.classList.remove('is-success'); + } + }, 700); + addSourceCardToMixer(card); + }; + + cardEl.onclick = (e) => { + if (e.target.closest('button') || e.target.closest('input')) return; + handleCardAdd(); + }; + dockBtn.onclick = (e) => { e.stopPropagation(); - addSourceCardToMixer(card); + handleCardAdd(); }; }); } @@ -1310,6 +1381,22 @@ function buildPromptComposer(owner, container, options = {}) { updateOutputPreview(); }; }); + + // Interactive Auto-snap Dock at end of track + const snapDock = text(blocksContainer, 'div', '', 'anomalous-assembly-snap-dock'); + snapDock.innerHTML = ` +
+ + + + ${window.anomalous_browser_lang === 'zh' ? '拖拽卡片至此自动贴合至末尾' : 'Drop card here to snap to bottom'} +
+ `; + setupDropzoneListeners(snapDock); + snapDock.onclick = () => { + leftSearch?.focus?.(); + showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '点击左侧词卡即可直接加入此处' : 'Click any card on left to add here'); + }; } function setupDropzoneListeners(el) { @@ -1327,8 +1414,19 @@ function buildPromptComposer(owner, container, options = {}) { if (jsonStr) { try { addSourceCardToMixer(JSON.parse(jsonStr)); + return; } catch (err) {} } + if (draggedBlockId) { + const fromIndex = draft.plan.parts.findIndex(p => p.id === draggedBlockId); + if (fromIndex >= 0) { + const [moved] = draft.plan.parts.splice(fromIndex, 1); + draft.plan.parts.push(moved); + syncDraftSynthesizedText(draft); + renderBlocksList(); + updateOutputPreview(); + } + } }; } diff --git a/web/styles.css b/web/styles.css index 3565d5e..3066279 100644 --- a/web/styles.css +++ b/web/styles.css @@ -10836,49 +10836,86 @@ dialog.anomalous-gallery-viewer::backdrop { z-index: 1; } -/* Individual Source Prompt Card (Restrained Dark Grey Base with Category Left Border) */ -.anomalous-source-prompt-card { +/* Tip Banner for Prompt Studio Usage Guidance */ +.anomalous-workbench-tip-banner { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + margin: 2px 0 8px; + background: rgba(56, 189, 248, 0.07); + border: 1px solid rgba(56, 189, 248, 0.2); border-radius: var(--amb-radius-control, 6px); - background: var(--amb-bg-card, #24262E); - border: 1px solid var(--amb-border, rgba(255, 255, 255, 0.08)); - border-left: 3.5px solid #64748b; + font-size: 0.72rem; + color: #93c5fd; + line-height: 1.35; + user-select: none; +} + +.anomalous-workbench-tip-banner .anomalous-tip-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + color: #38bdf8; +} + +/* Individual Source Prompt Card (Tactile Skewed Glassmorphic Card Aesthetics) */ +.anomalous-source-prompt-card { + border-radius: 8px; + background: linear-gradient(180deg, #262935 0%, #1e2029 100%); + border: 1px solid rgba(255, 255, 255, 0.11); + border-left: 4px solid #64748b; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; cursor: grab; user-select: none; - transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.08); + transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; } .anomalous-source-prompt-card.is-cat-base { border-left-color: #38bdf8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(56, 189, 248, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); } .theme-abyssal-scarlet .anomalous-source-prompt-card.is-cat-base { border-left-color: #dc143c; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(220, 20, 60, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); } .anomalous-source-prompt-card.is-cat-style { border-left-color: #c084fc; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(192, 132, 252, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); } .anomalous-source-prompt-card.is-cat-subject { border-left-color: #4ade80; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(74, 222, 128, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); } .anomalous-source-prompt-card.is-cat-trigger { border-left-color: #fb923c; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(251, 146, 60, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); } .anomalous-source-prompt-card:hover { - background: var(--amb-bg-card-hover, #2d303a); - border-color: var(--amb-border-hover, rgba(255, 255, 255, 0.16)); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + background: linear-gradient(180deg, #2e3242 0%, #232633 100%); + border-color: rgba(255, 255, 255, 0.22); + transform: translateY(-2px); + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .anomalous-source-prompt-card:active { cursor: grabbing; + transform: translateY(0); +} + +.anomalous-source-prompt-card.is-clicked { + transform: scale(0.97); + border-color: #38bdf8 !important; + box-shadow: 0 0 16px rgba(56, 189, 248, 0.45) !important; } .anomalous-source-prompt-card.is-dragging-source { @@ -10892,9 +10929,25 @@ dialog.anomalous-gallery-viewer::backdrop { gap: 6px; } +.anomalous-source-card-grip { + color: #64748b; + font-size: 11px; + line-height: 1; + margin-right: 1px; + letter-spacing: -1px; + cursor: grab; + opacity: 0.5; + transition: opacity 0.15s ease, color 0.15s ease; +} + +.anomalous-source-prompt-card:hover .anomalous-source-card-grip { + opacity: 0.95; + color: #94a3b8; +} + .anomalous-source-card-badge { - padding: 2px 6px; - border-radius: 4px; + padding: 2px 7px; + border-radius: 9999px; font-size: 0.66rem; font-weight: 600; border: 1px solid transparent; @@ -10902,8 +10955,9 @@ dialog.anomalous-gallery-viewer::backdrop { } .anomalous-source-card-title { - font-size: 0.8rem; - color: #e2e8f0; + font-size: 0.82rem; + font-weight: 600; + color: #f1f5f9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -10911,16 +10965,18 @@ dialog.anomalous-gallery-viewer::backdrop { } .anomalous-source-card-snippet { - font-size: 0.74rem; - color: #94a3b8; - line-height: 1.4; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.72rem; + color: #cbd5e1; + line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; - background: rgba(0, 0, 0, 0.2); - padding: 4px 6px; - border-radius: 4px; + background: rgba(0, 0, 0, 0.32); + border: 1px solid rgba(255, 255, 255, 0.06); + padding: 5px 8px; + border-radius: 5px; } .anomalous-source-card-footer { @@ -10934,7 +10990,10 @@ dialog.anomalous-gallery-viewer::backdrop { .anomalous-source-card-drag-hint { color: #64748b; - font-size: 0.68rem; + font-size: 0.66rem; + display: inline-flex; + align-items: center; + gap: 3px; } .anomalous-source-card-actions { @@ -10960,16 +11019,31 @@ dialog.anomalous-gallery-viewer::backdrop { } .anomalous-source-action-btn.is-dock { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.14); - color: var(--amb-text-main, #f1f5f9); - font-weight: 500; + background: rgba(56, 189, 248, 0.12); + border-color: rgba(56, 189, 248, 0.35); + color: #38bdf8; + font-weight: 600; + border-radius: 5px; + padding: 3px 8px; + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 0.7rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); } .anomalous-source-action-btn.is-dock:hover { - background: var(--amb-bg-card-hover, rgba(255, 255, 255, 0.14)); - border-color: var(--amb-border-hover, rgba(255, 255, 255, 0.25)); - color: #ffffff; + background: #38bdf8; + color: #0b1329; + border-color: #38bdf8; + box-shadow: 0 0 12px rgba(56, 189, 248, 0.45); +} + +.anomalous-source-action-btn.is-dock.is-success { + background: #22c55e !important; + border-color: #22c55e !important; + color: #ffffff !important; + box-shadow: 0 0 12px rgba(34, 197, 94, 0.5) !important; } .anomalous-source-action-btn.is-save-lib { @@ -11090,6 +11164,66 @@ dialog.anomalous-gallery-viewer::backdrop { background: rgba(45, 10, 20, 0.6); } +/* Interactive Auto-snap Dock at the bottom of Assembler blocks */ +.anomalous-assembly-snap-dock { + display: flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 8px 14px; + border: 1.5px dashed rgba(255, 255, 255, 0.14); + border-radius: 8px; + background: rgba(0, 0, 0, 0.15); + cursor: pointer; + user-select: none; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + margin-top: 6px; + color: #94a3b8; + font-size: 0.74rem; +} + +.anomalous-assembly-snap-dock:hover { + border-color: rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.04); + color: #e2e8f0; +} + +.anomalous-assembly-snap-dock.is-drag-over, +.anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock { + border-color: #38bdf8 !important; + background: rgba(56, 189, 248, 0.12) !important; + box-shadow: 0 0 18px rgba(56, 189, 248, 0.25), inset 0 0 10px rgba(56, 189, 248, 0.05) !important; + color: #38bdf8 !important; + transform: scale(1.01); +} + +.theme-abyssal-scarlet .anomalous-assembly-snap-dock.is-drag-over, +.theme-abyssal-scarlet .anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock { + border-color: #dc143c !important; + background: rgba(220, 20, 60, 0.15) !important; + box-shadow: 0 0 18px rgba(220, 20, 60, 0.3) !important; + color: #ff6b81 !important; +} + +.anomalous-snap-dock-content { + display: flex; + align-items: center; + gap: 6px; + font-weight: 500; + pointer-events: none; +} + +.anomalous-snap-icon { + width: 14px; + height: 14px; + transition: transform 0.2s ease; +} + +.anomalous-assembly-snap-dock.is-drag-over .anomalous-snap-icon, +.anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock .anomalous-snap-icon { + transform: rotate(90deg) scale(1.15); +} + .anomalous-workbench-sub-actions { display: flex; align-items: center; @@ -12290,3 +12424,10 @@ button:hover .anomalous-btn-icon { padding: 1px 4px !important; font-size: 0.68rem !important; } + +#anomalous-container.anomalous-docked .anomalous-assembly-snap-dock { + min-height: 32px !important; + padding: 4px 8px !important; + font-size: 0.68rem !important; + margin-top: 3px !important; +}