From db09ef5dcc8d2daadedaf0784d18c63ec0b31280 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sat, 12 Sep 2026 23:03:40 +0800 Subject: [PATCH] feat(prompt-studio): enhance card tactile affordance and auto-snap drop dock --- ARCHITECTURE.md | 3 +- web/modules/ui_prompt_composer.js | 118 ++++++++++++++++-- web/styles.css | 195 +++++++++++++++++++++++++----- 3 files changed, 278 insertions(+), 38 deletions(-) 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 = ` +