diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 70002ec..977e9b6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -152,29 +152,20 @@ DOM or live LiteGraph state. - `translation_service.js` owns atomic prompt translation logic and language intelligence; bridges directly to the ComfyUI backend `POST /anomalous/translate` with multi-engine fallback (DeepL API Key ➔ Google Chrome Dictionary Client (`dict-chrome-ex`, bypasses 429 rate limits) ➔ MyMemory Translation Engine); features `hasChinese` heuristic detection, automatic target language deduction (Chinese ➔ English, English ➔ Simplified Chinese), - in-memory LRU caching (500 entries) preventing redundant network roundtrips, strict error propagation, and multi-separator prompt tag splitting (`splitPromptTags`). - `ui_prompt_translator.js` owns the standalone Quick Prompt Translator (提示词翻译助手); - mounted directly to `document.body` with high z-index (999999) glassmorphism styling; registered in the Toolbox Hub (`openPromptTranslator`); + mounted directly to `document.body` as an elevated sidebar drawer (`.anomalous-translator-modal.is-sidebar`, z-index 999999); registered in the Toolbox Hub (`openPromptTranslator`); features live language selection, one-click ComfyUI canvas node prompt reading (`readSelectedNodePrompt`), instant bilingual translation, interactive tag breakdown pill chips (`.anomalous-translator-chip`), - one-click copy, direct write to selected canvas text node (`writeToSelectedNode` via `applyNodeMaterialValues`), and one-click dispatch into Prompt Studio. -- `ui_prompt_composer.js` owns the Prompt Studio Dual-Column Workbench (提示词工坊左右双分栏工作台), an independent tool accessed exclusively from the Toolbox Hub (`openPromptStudio`); - rendered in its dedicated container (`owner.promptStudioContainer`) with its own header title and close button (`[✕]`), completely decoupled from the Material Library; - provides a clean, professional studio workspace (`.anomalous-prompt-workbench`) stripped of gaudy neon effects, cyberpunk backgrounds, and pulsing glow animations; - 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 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 - (`➕ 新建`, with explicit positive/negative role radios, built-in one-click `[🌐 翻译]` bilingual bridge, and `/anomalous/save_prompt_plan` backend persistence; temporary cards - show `[未保存]` badge with one-click `💾 存入库`); - 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, dedicated block-level one-click `[🌐]` translation micro-buttons, 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 (`[📋]`), one-click assembled prompt translation 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; + one-click copy, direct write to selected canvas text node (`writeToSelectedNode` via `applyNodeMaterialValues`), dock-side toggling (`[⇤ / ⇥]`), width resizing, and one-click dispatch into Prompt Studio. +- `ui_prompt_composer.js` owns the Prompt Studio Minimalist Workbench (提示词工坊极简侧边栏抽屉与拼装台): + an independent, high-efficiency companion tool launched from the Toolbox Hub (`openPromptStudio`); + rendered directly into `document.body` as an elevated sidebar drawer (`.anomalous-prompt-studio-drawer`, z-index 999991) with canvas passthrough (`pointer-events: none` backdrop, allowing uninterrupted graph navigation); + features Canvas Focus Mode: opening the studio drawer automatically collapses the heavy master browser to preserve 80%+ canvas workspace; + features dock-side switching (`[⇤ / ⇥]`, persisted to `localStorage.anomalous_studio_dock_side`), width resize handle (400px~800px, persisted to `localStorage.anomalous_studio_sidebar_width`), and ESC key dismissal; + features a resilient Minimalist Dual-Column Layout: Left Column (145px~155px) hosts high-density compact prompt pills (`.anomalous-source-card-compact`) with category dot accents and instant 1-tap track injection; Right Column expands to take all remaining width as the primary Assembly Stage (`.anomalous-assembly-track`); + each assembled block card features category pills, title editing, a quick translate button, and an interactive **Weight Wheel Pill** (`.anomalous-block-weight-pill`) supporting instant mouse wheel scrolling (+/- 0.05 step) and +/- click adjustments; + eliminates cumbersome static textareas: Assembled output uses a zero-footprint **Floating Action Dock** (`.anomalous-floating-action-dock`) that is hidden (0px height) when empty and smoothly slides up (38px height) when blocks exist, offering real-time token/block stats, `[🚀 写入节点]`, `[📋 复制]`, and `[⛶ 展开大窗]`; + clicking `[⛶]` launches an independent centered **Prompt Inspector Modal** (`.anomalous-prompt-inspector-modal`, 780x520px, z-index 999999) for deep full-text inspection, bilingual translation, tag normalization (`[🧹 规范化]`), and direct write-back to canvas text nodes; `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, `categorizePromptSnippet`, `smartSortPromptBlocks`, `assemblePromptBlocks`, as well as backward-compatible text joining; diff --git a/web/modules/ui_prompt_composer.js b/web/modules/ui_prompt_composer.js index 4b7bc10..fb9a721 100644 --- a/web/modules/ui_prompt_composer.js +++ b/web/modules/ui_prompt_composer.js @@ -129,40 +129,310 @@ export function addPromptToDraft(owner, name, positive, negative = '') { showPromptComposer(owner); } +export function openPromptInspectorModal(owner, draft, defaultRole = 'positive', onApplyToNode = null) { + const existing = document.querySelector('.anomalous-prompt-inspector-overlay'); + if (existing) existing.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'anomalous-prompt-inspector-overlay'; + + const modal = document.createElement('div'); + modal.className = 'anomalous-prompt-inspector-modal'; + overlay.appendChild(modal); + + let currentRole = defaultRole || 'positive'; + + // Header + const header = document.createElement('header'); + header.className = 'anomalous-inspector-header'; + const h3 = document.createElement('h3'); + h3.textContent = window.anomalous_browser_lang === 'zh' ? '🔍 提示词深度全览与精修' : '🔍 Prompt Inspector & Editor'; + h3.style.margin = '0'; + h3.style.fontSize = '0.92rem'; + h3.style.color = '#e2e8f0'; + header.appendChild(h3); + + // Role Tabs + const tabsWrap = document.createElement('div'); + tabsWrap.style.display = 'flex'; + tabsWrap.style.gap = '6px'; + + const posTab = document.createElement('button'); + posTab.className = `anomalous-btn-ghost anomalous-btn-sm${currentRole === 'positive' ? ' is-active' : ''}`; + posTab.textContent = `🔆 ${window.anomalous_browser_lang === 'zh' ? '正向提示词' : 'Positive'}`; + + const negTab = document.createElement('button'); + negTab.className = `anomalous-btn-ghost anomalous-btn-sm${currentRole === 'negative' ? ' is-active' : ''}`; + negTab.textContent = `🌙 ${window.anomalous_browser_lang === 'zh' ? '负向提示词' : 'Negative'}`; + + tabsWrap.appendChild(posTab); + tabsWrap.appendChild(negTab); + header.appendChild(tabsWrap); + + const closeBtn = document.createElement('button'); + closeBtn.className = 'anomalous-btn-ghost anomalous-btn-sm'; + closeBtn.textContent = '✕'; + closeBtn.title = window.anomalous_browser_lang === 'zh' ? '关闭 (Esc)' : 'Close (Esc)'; + closeBtn.onclick = () => overlay.remove(); + header.appendChild(closeBtn); + + // Body + const body = document.createElement('div'); + body.className = 'anomalous-inspector-body'; + + const textarea = document.createElement('textarea'); + textarea.className = 'anomalous-inspector-textarea'; + textarea.spellcheck = false; + + syncDraftSynthesizedText(draft); + textarea.value = draft?.plan?.[currentRole] || ''; + + const updateStats = () => { + const val = textarea.value; + const words = val.split(/[,,\s\n]+/).filter(Boolean).length; + statsEl.textContent = `${val.length} ${window.anomalous_browser_lang === 'zh' ? '字符' : 'chars'} · ~${words} ${window.anomalous_browser_lang === 'zh' ? '词' : 'words'}`; + }; + + const updateTabStyles = () => { + posTab.classList.toggle('is-active', currentRole === 'positive'); + negTab.classList.toggle('is-active', currentRole === 'negative'); + textarea.value = draft?.plan?.[currentRole] || ''; + updateStats(); + }; + + posTab.onclick = () => { currentRole = 'positive'; updateTabStyles(); }; + negTab.onclick = () => { currentRole = 'negative'; updateTabStyles(); }; + + body.appendChild(textarea); + + // Footer + const footer = document.createElement('footer'); + footer.className = 'anomalous-inspector-footer'; + + const statsEl = document.createElement('div'); + statsEl.style.fontSize = '0.75rem'; + statsEl.style.color = '#94a3b8'; + statsEl.style.fontFamily = 'monospace'; + + textarea.oninput = () => { + if (draft?.plan) draft.plan[currentRole] = textarea.value; + updateStats(); + }; + updateStats(); + + footer.appendChild(statsEl); + + const actions = document.createElement('div'); + actions.style.display = 'flex'; + actions.style.gap = '6px'; + + // Normalize formatting + const normBtn = document.createElement('button'); + normBtn.className = 'anomalous-btn-ghost anomalous-btn-sm'; + normBtn.textContent = `🧹 ${window.anomalous_browser_lang === 'zh' ? '规范化' : 'Normalize'}`; + normBtn.title = window.anomalous_browser_lang === 'zh' ? '清理多余顿号、分号与重复逗号' : 'Clean formatting and punctuation'; + normBtn.onclick = () => { + const cleaned = textarea.value + .replace(/[,,、;;||\n\r]+/g, ', ') + .replace(/\s+,/g, ',') + .replace(/,\s*,/g, ', ') + .replace(/^\s*,\s*|\s*,\s*$/g, '') + .trim(); + textarea.value = cleaned; + if (draft?.plan) draft.plan[currentRole] = cleaned; + updateStats(); + }; + actions.appendChild(normBtn); + + // Translate + const transBtn = document.createElement('button'); + transBtn.className = 'anomalous-btn-ghost anomalous-btn-sm'; + transBtn.textContent = `🌐 ${window.anomalous_browser_lang === 'zh' ? '翻译' : 'Translate'}`; + transBtn.onclick = async () => { + const raw = textarea.value.trim(); + if (!raw) return; + const orig = transBtn.textContent; + transBtn.textContent = '⏳'; + try { + const res = await translatePromptText(raw); + if (res.ok && res.translated) { + textarea.value = res.translated; + if (draft?.plan) draft.plan[currentRole] = res.translated; + updateStats(); + } + } finally { + transBtn.textContent = orig; + } + }; + actions.appendChild(transBtn); + + // Copy + const copyBtn = document.createElement('button'); + copyBtn.className = 'anomalous-btn-ghost anomalous-btn-sm'; + copyBtn.textContent = `📋 ${window.anomalous_browser_lang === 'zh' ? '复制' : 'Copy'}`; + copyBtn.onclick = async () => { + await navigator.clipboard.writeText(textarea.value); + copyBtn.textContent = '✓'; + setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = `📋 ${window.anomalous_browser_lang === 'zh' ? '复制' : 'Copy'}`; }, 1200); + }; + actions.appendChild(copyBtn); + + // Apply to node & close + if (typeof onApplyToNode === 'function') { + const applyBtn = document.createElement('button'); + applyBtn.className = 'anomalous-btn-primary anomalous-btn-sm'; + applyBtn.textContent = `🚀 ${window.anomalous_browser_lang === 'zh' ? '写入节点并关闭' : 'Write Node & Close'}`; + applyBtn.onclick = async () => { + if (draft?.plan) draft.plan[currentRole] = textarea.value; + await onApplyToNode(currentRole, textarea.value); + overlay.remove(); + }; + actions.appendChild(applyBtn); + } + + footer.appendChild(actions); + + modal.appendChild(header); + modal.appendChild(body); + modal.appendChild(footer); + + overlay.onclick = (e) => { + if (e.target === overlay) overlay.remove(); + }; + + const onKey = (e) => { + if (e.key === 'Escape') { + window.removeEventListener('keydown', onKey); + overlay.remove(); + } + }; + window.addEventListener('keydown', onKey); + + document.body.appendChild(overlay); +} + +let activeStudioOverlay = null; +let activeStudioDrawer = null; + +export function closePromptStudio(owner) { + if (activeStudioOverlay) { + activeStudioOverlay.remove(); + activeStudioOverlay = null; + activeStudioDrawer = null; + } + if (owner?.sidePromptComposerControl) { + owner.sidePromptComposerControl = null; + } +} + export async function openPromptStudio(owner = this) { + closePromptStudio(owner); owner.closePromptImportDrawer?.(); - owner.recipeDetailFinish?.('closed'); - owner.modal?.classList.add('visible'); - if (owner.nbPanel?.style.display !== 'flex' && !owner.workspaceReturnState) { - owner.workspaceReturnState = Object.fromEntries([ - ['grid', owner.grid], ['detail', owner.detailPanel], ['gallery', owner.galleryPanel], - ['doctor', owner.doctorPanel], ['assistant', owner.assistantPanel], - ].filter(([, panel]) => panel).map(([key, panel]) => [key, panel.style.display])); - } - if (owner.nbPanel) owner.nbPanel.style.display = 'flex'; - for (const panel of [owner.grid, owner.detailPanel, owner.galleryPanel, owner.doctorPanel, owner.assistantPanel, owner.paramPanel]) { - if (panel) panel.style.display = 'none'; - } - if (owner.notebookContainer) owner.notebookContainer.style.display = 'none'; - if (owner.notebookBody) owner.notebookBody.style.display = 'none'; - if (owner.recipeView) owner.recipeView.style.display = 'none'; - if (owner.recipeContainer) owner.recipeContainer.style.display = 'none'; - if (owner.materialContainer) owner.materialContainer.style.display = 'none'; - if (owner.materialView) owner.materialView.style.display = 'none'; - if (!owner.promptStudioContainer) { - owner.promptStudioContainer = text(owner.nbPanel, 'div', '', 'anomalous-nb-container anomalous-prompt-studio-container'); + // Canvas Focus Mode: If left master browser is visible, auto-collapse it so canvas has 80%+ space + if (owner?.modal?.classList.contains('visible')) { + if (typeof owner.close === 'function') { + owner.close(); + } } - owner.promptStudioContainer.style.display = 'flex'; - owner.promptStudioContainer.replaceChildren(); - owner.promptComposerView?.remove(); - owner.promptComposerView = null; + const overlay = document.createElement('div'); + overlay.className = 'anomalous-prompt-studio-overlay'; + activeStudioOverlay = overlay; - buildPromptComposer(owner, owner.promptStudioContainer, { - isSideStudio: false, - onClose: () => owner.closeWorkspace?.(), + const drawer = document.createElement('aside'); + drawer.className = 'anomalous-prompt-studio-drawer'; + activeStudioDrawer = drawer; + overlay.appendChild(drawer); + + // Dock side persistence + const savedSide = localStorage.getItem('anomalous_studio_dock_side') || 'right'; + if (savedSide === 'left') { + drawer.classList.add('is-dock-left'); + } + + // Sidebar width persistence & resize handle + const savedWidth = localStorage.getItem('anomalous_studio_sidebar_width') || '520'; + drawer.style.width = `${Math.max(400, Math.min(window.innerWidth * 0.85, parseInt(savedWidth, 10)))}px`; + + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'anomalous-studio-resize-handle'; + resizeHandle.title = window.anomalous_browser_lang === 'zh' + ? '拖动调整侧边栏宽度,双击恢复默认' + : 'Drag to resize sidebar, double-click to reset'; + drawer.appendChild(resizeHandle); + + let isResizing = false; + let startX = 0; + let startWidth = 520; + resizeHandle.onmousedown = (e) => { + isResizing = true; + startX = e.clientX; + startWidth = drawer.getBoundingClientRect().width; + resizeHandle.classList.add('is-resizing'); + document.body.style.userSelect = 'none'; + document.body.style.cursor = 'ew-resize'; + + const onMouseMove = (moveEvt) => { + if (!isResizing) return; + const currentSide = drawer.classList.contains('is-dock-left') ? 'left' : 'right'; + let newWidth; + if (currentSide === 'left') { + const deltaX = moveEvt.clientX - startX; + newWidth = Math.max(380, Math.min(window.innerWidth * 0.85, startWidth + deltaX)); + } else { + const deltaX = startX - moveEvt.clientX; + newWidth = Math.max(380, Math.min(window.innerWidth * 0.85, startWidth + deltaX)); + } + drawer.style.width = `${newWidth}px`; + }; + + const onMouseUp = () => { + if (!isResizing) return; + isResizing = false; + resizeHandle.classList.remove('is-resizing'); + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + localStorage.setItem('anomalous_studio_sidebar_width', String(Math.round(drawer.getBoundingClientRect().width))); + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }; + + resizeHandle.ondblclick = () => { + drawer.style.width = '520px'; + localStorage.setItem('anomalous_studio_sidebar_width', '520'); + }; + + const onKeydown = (e) => { + if (e.key === 'Escape') { + const inspector = document.querySelector('.anomalous-prompt-inspector-overlay'); + if (inspector) return; + closePromptStudio(owner); + } + }; + window.addEventListener('keydown', onKeydown); + const origClose = () => { + window.removeEventListener('keydown', onKeydown); + closePromptStudio(owner); + }; + + const composer = buildPromptComposer(owner, drawer, { + isSideStudio: true, + isDrawerMode: true, + onClose: origClose, + onToggleDockSide: () => { + const isLeft = drawer.classList.toggle('is-dock-left'); + localStorage.setItem('anomalous_studio_dock_side', isLeft ? 'left' : 'right'); + return isLeft; + }, }); + owner.sidePromptComposerControl = composer; + + document.body.appendChild(overlay); } export async function showPromptComposer(owner, material) { @@ -312,10 +582,33 @@ function buildPromptComposer(owner, container, options = {}) { nameInput.oninput = () => { draft.name = nameInput.value; }; const topActions = text(topbar, 'div', '', 'anomalous-prompt-topbar-actions'); - const saveBtn = text(topActions, 'button', `💾 ${t('promptSavePlan')}`, 'anomalous-btn-primary anomalous-btn-sm'); - saveBtn.title = t('promptSavePlan') || '保存方案至素材库'; - // "More" Dropdown Menu (holds New Draft, Tags, Export) + // Quick Extract from Selected Canvas Node + const extractQuickBtn = text(topActions, 'button', '', 'anomalous-btn-ghost anomalous-btn-sm'); + extractQuickBtn.innerHTML = '🎯'; + extractQuickBtn.title = window.anomalous_browser_lang === 'zh' + ? '从画布选中节点一键吸取提示词' + : 'Extract prompt from selected node'; + extractQuickBtn.onclick = () => extractPromptsFromSelectedNode(true); + + // Dock Side Switch (Left / Right) + if (options.isDrawerMode && typeof options.onToggleDockSide === 'function') { + const dockSideBtn = text(topActions, 'button', '', 'anomalous-btn-ghost anomalous-btn-sm'); + const isLeft = () => activeStudioDrawer?.classList.contains('is-dock-left'); + const updateDockBtn = () => { + dockSideBtn.innerHTML = isLeft() ? '⇥' : '⇤'; + dockSideBtn.title = isLeft() + ? (window.anomalous_browser_lang === 'zh' ? '停靠至右侧' : 'Dock Right') + : (window.anomalous_browser_lang === 'zh' ? '停靠至左侧' : 'Dock Left'); + }; + updateDockBtn(); + dockSideBtn.onclick = () => { + options.onToggleDockSide(); + updateDockBtn(); + }; + } + + // "More" Dropdown Menu (holds Save, New Draft, Tags, Export, Material Sync) const moreWrap = text(topActions, 'div', '', 'anomalous-prompt-more-wrap'); moreWrap.style.position = 'relative'; moreWrap.style.display = 'inline-flex'; @@ -333,6 +626,12 @@ function buildPromptComposer(owner, container, options = {}) { moreMenu.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.6)'; moreMenu.style.padding = '4px'; + const saveBtn = text(moreMenu, 'button', `💾 ${t('promptSavePlan')}`, 'anomalous-btn-ghost anomalous-btn-sm'); + saveBtn.style.width = '100%'; + saveBtn.style.justifyContent = 'flex-start'; + saveBtn.style.border = 'none'; + saveBtn.title = t('promptSavePlan') || '保存方案至素材库'; + const newBtn = text(moreMenu, 'button', `✨ ${t('promptNewDraft')}`, 'anomalous-btn-ghost anomalous-btn-sm'); newBtn.style.width = '100%'; newBtn.style.justifyContent = 'flex-start'; @@ -357,6 +656,15 @@ function buildPromptComposer(owner, container, options = {}) { exportBtn.style.justifyContent = 'flex-start'; exportBtn.style.border = 'none'; + const syncLibBtn = text(moreMenu, 'button', `📥 ${window.anomalous_browser_lang === 'zh' ? '同步素材库' : 'Sync Library'}`, 'anomalous-btn-ghost anomalous-btn-sm'); + syncLibBtn.style.width = '100%'; + syncLibBtn.style.justifyContent = 'flex-start'; + syncLibBtn.style.border = 'none'; + syncLibBtn.onclick = () => { + closeMoreMenu(); + syncMaterialsIntoSourceDeck(); + }; + moreBtn.onclick = (e) => { e.stopPropagation(); const isHidden = moreMenu.style.display === 'none'; @@ -526,104 +834,85 @@ function buildPromptComposer(owner, container, options = {}) { } }; - // 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'); - - // 1. Slim Dock Summary & Target Injection Bar (Always visible) - const outputSummary = text(outputDeck, 'div', '', 'anomalous-mixer-output-summary'); - const summaryLeft = text(outputSummary, 'div', '', 'anomalous-mixer-summary-left'); - const outputStats = text(summaryLeft, 'div', '', 'anomalous-mixer-output-stats'); - - const expandOutputBtn = text(summaryLeft, 'button', window.anomalous_browser_lang === 'zh' ? '📄 最终文本 ▾' : '📄 View Text ▾', 'anomalous-btn-ghost anomalous-btn-sm anomalous-btn-toggle-output'); - expandOutputBtn.title = window.anomalous_browser_lang === 'zh' ? '展开/收起最终合成提示词文本' : 'Expand/collapse assembled text'; - - const copyOutputBtn = text(summaryLeft, 'button', `📋`, 'anomalous-btn-ghost anomalous-btn-sm anomalous-btn-quick-copy'); - copyOutputBtn.title = window.anomalous_browser_lang === 'zh' ? '复制当前合成的提示词' : 'Copy assembled prompt text'; - - const translateOutputBtn = text(summaryLeft, 'button', `🌐`, 'anomalous-btn-ghost anomalous-btn-sm anomalous-btn-quick-translate'); - translateOutputBtn.title = window.anomalous_browser_lang === 'zh' ? '一键翻译当前合成词并复制到剪贴板' : 'Translate assembled prompt & copy'; - translateOutputBtn.onclick = async (e) => { - e.stopPropagation(); - const currentText = (draft?.plan?.[activeTab] || '').trim(); - if (!currentText) return; - const origIcon = translateOutputBtn.textContent; - translateOutputBtn.textContent = '⏳'; - try { - const res = await translatePromptText(currentText); - if (res.ok && res.translated) { - await navigator.clipboard.writeText(res.translated); - showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? `✓ 译文已复制至剪贴板: ${res.translated.slice(0, 30)}...` : `✓ Translated & copied: ${res.translated.slice(0, 30)}...`); - } else { - showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? `翻译失败: ${res.error || '网络错误'}` : `Translation failed: ${res.error || 'Network error'}`); - } - } catch (err) { - console.warn(err); - } finally { - translateOutputBtn.textContent = origIcon; - } - }; - - // Target Node Direct Write Bar placed INSIDE outputSummary on the right side - const targetBar = text(outputSummary, 'div', '', 'anomalous-prompt-target-bar'); + // ------------------------------------------------------------------------- + // Node Direct Write Handler + // ------------------------------------------------------------------------- const targetWidgetIndexByNodeId = {}; - - // 2. Expandable Drawer (reveals full textarea and canvas drag dock) - const outputDrawer = text(outputDeck, 'div', '', 'anomalous-mixer-output-drawer'); - outputDrawer.style.display = 'none'; - - const outputBody = text(outputDrawer, 'div', '', 'anomalous-mixer-output-body'); - const outputTextarea = text(outputBody, 'textarea', '', 'anomalous-mixer-output-textarea'); - outputTextarea.readOnly = true; - outputTextarea.rows = 2; - outputTextarea.oninput = () => {}; - - const outputFooter = text(outputDrawer, 'div', '', 'anomalous-mixer-output-footer'); - const dragHint = text(outputFooter, 'div', '', 'anomalous-mixer-drag-hint'); - dragHint.innerHTML = `🖐️ ${window.anomalous_browser_lang === 'zh' ? '按住此预览坞' : 'Drag this deck'} ${window.anomalous_browser_lang === 'zh' ? '直接丢入 ComfyUI 画布节点' : 'onto canvas node'}`; - - function updateOutputDeckState() { - outputDeck.style.display = 'flex'; - - if (isOutputExpanded) { - outputDeck.classList.remove('is-collapsed'); - outputDrawer.style.display = 'flex'; - expandOutputBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '收起预览 ▲' : 'Collapse ▲'; - expandOutputBtn.classList.add('is-active'); - } else { - outputDeck.classList.add('is-collapsed'); - outputDrawer.style.display = 'none'; - expandOutputBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '📄 最终文本 ▾' : '📄 View Text ▾'; - expandOutputBtn.classList.remove('is-active'); + const applyPromptToCurrentNode = async (role, promptContent, triggerBtn = null) => { + const node = selectedMaterialNode(app); + if (!node) { + anomalousAlert(window.anomalous_browser_lang === 'zh' + ? '💡 请先在 ComfyUI 画布上点击选中一个提示词节点(例如 CLIPTextEncode)!' + : '💡 Please select a prompt node (e.g. CLIPTextEncode) on the ComfyUI canvas first!'); + return; } - } + const targets = promptWidgetTargets(node); + if (!targets.length) { + anomalousAlert(window.anomalous_browser_lang === 'zh' + ? '⚠️ 选中的节点中未找到可写入的提示词文本输入!' + : '⚠️ No writable prompt text widget found in selected node!'); + return; + } + const widgetIndex = targetWidgetIndexByNodeId[node.id] !== undefined ? targetWidgetIndexByNodeId[node.id] : targets[0].index; + try { + const value = joinPromptText(node.widgets[widgetIndex].value, promptContent, posSelect.value); + showMaterialApplication(view, applyNodeMaterialValues(app, node, [{ index: widgetIndex, value }]), node); + if (triggerBtn) { + const orig = triggerBtn.textContent; + triggerBtn.textContent = '✅ ' + (window.anomalous_browser_lang === 'zh' ? '已写入' : 'Written'); + setTimeout(() => { if (triggerBtn.isConnected) triggerBtn.textContent = orig; }, 1200); + } else { + showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '✅ 提示词已成功写入节点!' : '✅ Prompt written to node!'); + } + } catch (error) { + await anomalousAlert(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message)); + } + }; - expandOutputBtn.onclick = (e) => { + // ------------------------------------------------------------------------- + // Floating Action Dock (Minimalist & Slides Up Only When Content Exists) + // ------------------------------------------------------------------------- + const floatingDock = text(rightPanel, 'div', '', 'anomalous-floating-action-dock'); + floatingDock.style.display = 'none'; // Initially zero-height, hidden until content is added + + const dockLeft = text(floatingDock, 'div', '', 'anomalous-floating-dock-left'); + const dockStats = text(dockLeft, 'span', '', 'anomalous-dock-stats-text'); + + const dockRight = text(floatingDock, 'div', '', 'anomalous-floating-dock-right'); + + // 1. Write Node Button + const writeNodeBtn = text(dockRight, 'button', `🚀 ${window.anomalous_browser_lang === 'zh' ? '写入节点' : 'Write Node'}`, 'anomalous-btn-primary anomalous-btn-sm'); + writeNodeBtn.title = window.anomalous_browser_lang === 'zh' ? '将当前合成提示词写入选中的 ComfyUI 节点' : 'Write assembled prompt to selected node'; + writeNodeBtn.onclick = async () => { + syncDraftSynthesizedText(draft); + const compiledText = (draft.plan[activeTab] || '').trim(); + if (!compiledText) return; + await applyPromptToCurrentNode(activeTab, compiledText, writeNodeBtn); + }; + + // 2. Quick Copy Button + const copyOutputBtn = text(dockRight, 'button', '📋', 'anomalous-btn-ghost anomalous-btn-sm'); + copyOutputBtn.title = window.anomalous_browser_lang === 'zh' ? '复制当前合成的提示词' : 'Copy assembled prompt text'; + copyOutputBtn.onclick = async (e) => { e.stopPropagation(); - isOutputExpanded = !isOutputExpanded; - updateOutputDeckState(); + syncDraftSynthesizedText(draft); + const compiledText = (draft.plan[activeTab] || '').trim(); + if (!compiledText) return; + await navigator.clipboard.writeText(compiledText); + copyOutputBtn.textContent = '✓'; + setTimeout(() => { if (copyOutputBtn.isConnected) copyOutputBtn.textContent = '📋'; }, 1200); + showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '已复制至剪贴板' : 'Copied to clipboard'); }; - outputSummary.onclick = (e) => { - if (e.target.closest('button') || e.target.closest('select') || e.target.closest('.anomalous-prompt-target-bar')) return; - isOutputExpanded = !isOutputExpanded; - updateOutputDeckState(); + // 3. Popout Full Inspector Modal Button + const popoutBtn = text(dockRight, 'button', '⛶', 'anomalous-btn-ghost anomalous-btn-sm'); + popoutBtn.title = window.anomalous_browser_lang === 'zh' ? '弹出独立大弹窗全览与精修' : 'Pop out full inspector modal'; + popoutBtn.onclick = () => { + openPromptInspectorModal(owner, draft, activeTab, async (role, textVal) => { + await applyPromptToCurrentNode(role, textVal); + }); }; - // Bind output deck drag once on mount - bindMaterialDrag(outputDeck, owner, { - payload: () => { - const compiledText = (draft?.plan?.[activeTab] || '').trim(); - return compiledText ? { - content: compiledText, - position: posSelect.value, - dragHint: activeTab === 'positive' - ? (window.anomalous_browser_lang === 'zh' ? '✨ 拖拽合成正面词至画布节点' : '✨ Drag Assembled Positive onto Node') - : (window.anomalous_browser_lang === 'zh' ? '🚫 拖拽合成负面词至画布节点' : '🚫 Drag Assembled Negative onto Node'), - } : null; - }, - accepts: node => promptWidgetTargets(node).length > 0, - drop: (node, data, graph) => applyPromptDrop(node, data, graph, targetBar), - }); // ------------------------------------------------------------------------- // FEATURE IMPLEMENTATIONS: Node Extraction & Material Sync @@ -1035,11 +1324,12 @@ function buildPromptComposer(owner, container, options = {}) { } filtered.forEach(card => { - const cardEl = text(sourceCardsList, 'article', '', `anomalous-source-prompt-card is-cat-${card.category}`); + const catMeta = CATEGORY_META[card.category] || CATEGORY_META.subject; + const cardEl = text(sourceCardsList, 'div', '', `anomalous-source-card-compact is-cat-${card.category}`); cardEl.setAttribute('draggable', 'true'); - cardEl.title = window.anomalous_browser_lang === 'zh' ? '点击直接加入右侧拼装台(也可按住拖拽)' : 'Click to add to stage (or drag)'; + cardEl.title = `[${window.anomalous_browser_lang === 'zh' ? catMeta.zh : catMeta.en}] ${card.title}\n${card.content}`; - // Drag Start -> transfer prompt card payload + // Drag Start cardEl.ondragstart = (e) => { const payload = { title: card.title, @@ -1059,94 +1349,16 @@ function buildPromptComposer(owner, container, options = {}) { workbenchGrid.classList.remove('is-source-dragging'); }; - // Card Header - const header = text(cardEl, 'div', '', 'anomalous-source-card-header'); - const meta = CATEGORY_META[card.category] || CATEGORY_META.subject; + const dot = text(cardEl, 'span', '', 'anomalous-source-card-dot'); + dot.style.backgroundColor = catMeta.color; - const grip = text(header, 'span', '⠿', 'anomalous-source-card-grip'); - grip.title = window.anomalous_browser_lang === 'zh' ? '按住拖拽至拼装台' : 'Drag to assemble'; + const nameEl = text(cardEl, 'span', card.title, 'anomalous-source-card-name'); - 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; - badge.style.borderColor = meta.border; + const addIcon = text(cardEl, 'span', '+', 'anomalous-source-card-add-icon'); - // Explicit Role Badge (Positive / Negative) - const isNeg = card.role === 'negative'; - const roleBadge = text(header, 'span', isNeg ? (window.anomalous_browser_lang === 'zh' ? '负向' : 'Neg') : (window.anomalous_browser_lang === 'zh' ? '正向' : 'Pos'), 'anomalous-source-role-badge'); - roleBadge.style.cssText = `font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;background:${isNeg ? 'rgba(244,63,94,0.15)' : 'rgba(56,189,248,0.15)'};color:${isNeg ? '#fb7185' : '#38bdf8'};border:1px solid ${isNeg ? 'rgba(244,63,94,0.3)' : 'rgba(56,189,248,0.3)'};flex-shrink:0;`; - - if (card.persisted === false || (card.id.startsWith('node_') && !card.persisted)) { - const unsavedBadge = text(header, 'span', `[${t('promptCardUnsaved')}]`, 'anomalous-unsaved-badge'); - unsavedBadge.title = window.anomalous_browser_lang === 'zh' ? '临时提取词卡,尚未持久化到素材库' : 'Unsaved temporary card'; - } - - const title = text(header, 'strong', card.title, 'anomalous-source-card-title'); - title.title = card.title; - - // Card Body snippet (max 2 lines, legible font size) - const snippet = text(cardEl, 'div', card.content, 'anomalous-source-card-snippet'); - snippet.title = card.content; - - // 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'; - - const copyBtn = text(actions, 'button', '📋', 'anomalous-source-action-btn'); - copyBtn.title = t('copy'); - copyBtn.onclick = async (e) => { - e.stopPropagation(); - try { - await navigator.clipboard.writeText(card.content); - copyBtn.textContent = '✅'; - setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = '📋'; }, 1200); - } catch (err) { - await anomalousAlert(t('materialCopyError')); - } - }; - - if (card.persisted === false || (card.id.startsWith('node_') && !card.persisted)) { - const saveCardBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-save-lib'); - saveCardBtn.innerHTML = `${t('promptSaveCard')}`; - saveCardBtn.title = window.anomalous_browser_lang === 'zh' ? '将此卡片存入素材库' : 'Save this card to Material Library'; - saveCardBtn.onclick = async (e) => { - e.stopPropagation(); - await saveCardToLibrary(card, saveCardBtn); - }; - } - - const dockBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-dock'); - 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); + cardEl.onclick = () => { addSourceCardToMixer(card); }; - - cardEl.onclick = (e) => { - if (e.target.closest('button') || e.target.closest('input')) return; - handleCardAdd(); - }; - - dockBtn.onclick = (e) => { - e.stopPropagation(); - handleCardAdd(); - }; }); } @@ -1355,6 +1567,44 @@ function buildPromptComposer(owner, container, options = {}) { titleInput.value = block.title || ''; titleInput.oninput = () => { block.title = titleInput.value; }; + // Weight Adjustment Pill with Mouse Wheel & Click + let currentWeight = block.weight !== undefined ? block.weight : 1.0; + const weightPill = text(headerLeft, 'span', '', 'anomalous-block-weight-pill'); + weightPill.title = window.anomalous_browser_lang === 'zh' + ? '滚轮上下滑动调节权重,或点击加减' + : 'Scroll wheel or click +/- to adjust weight'; + + const decBtn = text(weightPill, 'button', '-', 'anomalous-block-weight-btn'); + const weightVal = text(weightPill, 'span', currentWeight.toFixed(2)); + const incBtn = text(weightPill, 'button', '+', 'anomalous-block-weight-btn'); + + const applyBlockWeight = (newW) => { + newW = Math.round(Math.max(0.1, Math.min(3.0, newW)) * 100) / 100; + currentWeight = newW; + block.weight = newW; + weightVal.textContent = newW.toFixed(2); + let clean = block.content.replace(/^\((.+):[0-9.]+\)$/, '$1').replace(/^\((.+)\)$/, '$1').trim(); + if (newW !== 1.0) { + block.content = `(${clean}:${newW.toFixed(2)})`; + } else { + block.content = clean; + } + const txtArea = blockEl.querySelector('.anomalous-mixer-block-textarea'); + if (txtArea) txtArea.value = block.content; + syncDraftSynthesizedText(draft); + updateOutputPreview(); + }; + + decBtn.onclick = (e) => { e.stopPropagation(); applyBlockWeight(currentWeight - 0.05); }; + incBtn.onclick = (e) => { e.stopPropagation(); applyBlockWeight(currentWeight + 0.05); }; + + weightPill.onwheel = (e) => { + e.preventDefault(); + e.stopPropagation(); + const delta = e.deltaY < 0 ? 0.05 : -0.05; + applyBlockWeight(currentWeight + delta); + }; + // Right Action micro buttons const headerRight = text(blockHeader, 'div', '', 'anomalous-mixer-block-header-right'); @@ -1504,16 +1754,19 @@ function buildPromptComposer(owner, container, options = {}) { function updateOutputPreview() { syncDraftSynthesizedText(draft); - const compiledText = draft.plan[activeTab] || ''; + const compiledText = (draft.plan[activeTab] || '').trim(); const currentRoleParts = draft.plan.parts.filter(p => p.role === activeTab); - outputStats.innerHTML = `${activeTab === 'negative' ? '🚫' : '✨'} ${compiledText.length}${window.anomalous_browser_lang === 'zh' ? '字' : 'c'} · ${currentRoleParts.length}${window.anomalous_browser_lang === 'zh' ? '块' : 'b'}`; - outputStats.title = activeTab === 'negative' - ? `${window.anomalous_browser_lang === 'zh' ? '负向' : 'Negative'}: ${compiledText.length} ${window.anomalous_browser_lang === 'zh' ? '字符' : 'chars'}, ${currentRoleParts.length} ${window.anomalous_browser_lang === 'zh' ? '块积木' : 'blocks'}` - : `${window.anomalous_browser_lang === 'zh' ? '正向' : 'Positive'}: ${compiledText.length} ${window.anomalous_browser_lang === 'zh' ? '字符' : 'chars'}, ${currentRoleParts.length} ${window.anomalous_browser_lang === 'zh' ? '块积木' : 'blocks'}`; - - outputTextarea.value = compiledText; - updateOutputDeckState(); + if (!currentRoleParts.length && !compiledText) { + floatingDock.style.display = 'none'; + } else { + floatingDock.style.display = 'flex'; + const words = compiledText.split(/[,,\s\n]+/).filter(Boolean).length; + dockStats.innerHTML = `${activeTab === 'negative' ? '🌙' : '🔆'} ${currentRoleParts.length} ${window.anomalous_browser_lang === 'zh' ? '块' : 'blocks'} · ~${words} ${window.anomalous_browser_lang === 'zh' ? '词' : 'words'}`; + dockStats.title = activeTab === 'negative' + ? `${window.anomalous_browser_lang === 'zh' ? '负向' : 'Negative'}: ${compiledText.length} ${window.anomalous_browser_lang === 'zh' ? '字符' : 'chars'}, ${currentRoleParts.length} ${window.anomalous_browser_lang === 'zh' ? '块积木' : 'blocks'}` + : `${window.anomalous_browser_lang === 'zh' ? '正向' : 'Positive'}: ${compiledText.length} ${window.anomalous_browser_lang === 'zh' ? '字符' : 'chars'}, ${currentRoleParts.length} ${window.anomalous_browser_lang === 'zh' ? '块积木' : 'blocks'}`; + } } // Smart Sort diff --git a/web/styles.css b/web/styles.css index 5f84362..c0bc04c 100644 --- a/web/styles.css +++ b/web/styles.css @@ -10555,78 +10555,31 @@ dialog.anomalous-gallery-viewer::backdrop { animation: anomalous-just-added 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; } +/* Resilient Responsive Layout for Prompt Workbench & Sidebar Drawer */ @container prompt-workbench (max-width: 850px) { .anomalous-prompt-workbench-grid { - display: flex !important; - flex-direction: column !important; - min-height: 0 !important; - overflow: hidden !important; - gap: 8px !important; - padding: 4px 6px 8px !important; - } - /* Upper Zone: 顺序拼装台 (Mixer Stage) */ - .anomalous-workbench-right-panel { - order: 1 !important; - flex: 1 1 50% !important; - min-height: 160px !important; - max-height: 55% !important; - display: flex !important; - flex-direction: column !important; - overflow: hidden !important; - padding: 8px 8px 6px !important; - } - /* Lower Zone: 候选词卡库 (Cards Deck) */ - .anomalous-workbench-left-panel { - order: 2 !important; - flex: 1 1 50% !important; - min-height: 160px !important; - max-height: 50% !important; - display: flex !important; - flex-direction: column !important; - overflow: hidden !important; - padding: 8px 8px 6px !important; - } - /* Compact 2-column cards grid in narrow mode */ - .anomalous-source-cards-list { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)) !important; + grid-template-columns: 145px minmax(0, 1fr) !important; gap: 6px !important; - align-content: start !important; + padding: 4px 6px 6px !important; } - .anomalous-source-prompt-card { - padding: 6px 8px !important; - gap: 4px !important; - cursor: pointer !important; + .anomalous-workbench-left-panel { + width: 145px !important; + padding: 6px 6px 6px !important; } - .anomalous-source-card-snippet { - -webkit-line-clamp: 1 !important; - font-size: 0.7rem !important; + .anomalous-workbench-right-panel { + padding: 6px 6px 6px !important; } } @media (max-width: 860px) { .anomalous-prompt-workbench-grid { - display: flex !important; - flex-direction: column !important; - min-height: 0 !important; - overflow: hidden !important; - gap: 8px !important; - padding: 4px 6px 8px !important; - } - .anomalous-workbench-right-panel { - order: 1 !important; - flex: 1 1 50% !important; - min-height: 160px !important; - max-height: 55% !important; - } - .anomalous-workbench-left-panel { - order: 2 !important; - flex: 1 1 50% !important; - min-height: 160px !important; - max-height: 50% !important; + grid-template-columns: 145px minmax(0, 1fr) !important; + gap: 6px !important; + padding: 4px 6px 6px !important; } } + /* Left & Right Panels Common */ .anomalous-workbench-left-panel, .anomalous-workbench-right-panel { @@ -12858,3 +12811,315 @@ button:hover .anomalous-btn-icon { color: #38bdf8; background: rgba(56, 189, 248, 0.18); } + +/* ========================================================================== + PROMPT STUDIO: Minimalist Sidebar Drawer & Floating Action Dock + ========================================================================== */ +.anomalous-prompt-studio-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 999990; + pointer-events: none; /* Canvas passthrough: pan/zoom canvas freely */ + overflow: hidden; +} + +.anomalous-prompt-studio-drawer { + position: absolute; + top: 0; + right: 0; + width: 520px; + max-width: 90vw; + height: 100vh; + background: var(--amb-bg-page, #121316); + border-left: 1px solid var(--amb-border, rgba(255, 255, 255, 0.12)); + box-shadow: -8px 0 32px rgba(0, 0, 0, 0.6); + pointer-events: auto; + display: flex; + flex-direction: column; + z-index: 999991; + box-sizing: border-box; +} + +.anomalous-prompt-studio-drawer.is-dock-left { + right: auto; + left: 0; + border-left: none; + border-right: 1px solid var(--amb-border, rgba(255, 255, 255, 0.12)); + box-shadow: 8px 0 32px rgba(0, 0, 0, 0.6); +} + +/* Edge Resize Handle */ +.anomalous-studio-resize-handle { + position: absolute; + top: 0; + left: -4px; + width: 8px; + height: 100%; + cursor: ew-resize; + z-index: 100; + transition: background 0.15s ease; +} + +.anomalous-prompt-studio-drawer.is-dock-left .anomalous-studio-resize-handle { + left: auto; + right: -4px; +} + +.anomalous-studio-resize-handle:hover, +.anomalous-studio-resize-handle.is-resizing { + background: #38bdf8; + box-shadow: 0 0 10px rgba(56, 189, 248, 0.6); +} + +/* Drawer Workbench Grid */ +.anomalous-prompt-studio-drawer .anomalous-prompt-workbench { + background: transparent; + padding: 0; + height: 100%; + display: flex; + flex-direction: column; +} + +.anomalous-prompt-studio-drawer .anomalous-prompt-workbench-grid { + display: grid !important; + grid-template-columns: 145px minmax(0, 1fr) !important; + flex-direction: row !important; + gap: 8px !important; + padding: 6px 8px 8px !important; + flex: 1; + min-height: 0; +} + +.anomalous-prompt-studio-drawer .anomalous-workbench-left-panel { + order: 1 !important; + flex: none !important; + width: 145px !important; + max-height: 100% !important; + padding: 6px 6px 8px !important; +} + +.anomalous-prompt-studio-drawer .anomalous-workbench-right-panel { + order: 2 !important; + flex: 1 !important; + max-height: 100% !important; + padding: 6px 8px 48px !important; /* reserve bottom space for floating dock */ + position: relative; +} + +/* Compact Source Cards */ +.anomalous-source-card-compact { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + user-select: none; + min-width: 0; + box-sizing: border-box; +} + +.anomalous-source-card-compact:hover { + background: rgba(56, 189, 248, 0.15); + border-color: rgba(56, 189, 248, 0.4); + transform: translateX(2px); +} + +.anomalous-source-card-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.anomalous-source-card-name { + font-size: 0.74rem; + color: #e2e8f0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +} + +.anomalous-source-card-add-icon { + font-size: 0.72rem; + color: #64748b; + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; +} + +.anomalous-source-card-compact:hover .anomalous-source-card-add-icon { + opacity: 1; + color: #38bdf8; +} + +/* Weight Wheel Pill in Assembled Blocks */ +.anomalous-block-weight-pill { + display: inline-flex; + align-items: center; + background: rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 12px; + padding: 0 4px; + font-size: 0.72rem; + font-family: monospace, sans-serif; + color: #38bdf8; + cursor: ns-resize; + user-select: none; + gap: 2px; + transition: all 0.15s; +} + +.anomalous-block-weight-pill:hover { + border-color: #38bdf8; + box-shadow: 0 0 8px rgba(56, 189, 248, 0.3); +} + +.anomalous-block-weight-btn { + background: transparent; + border: none; + color: #94a3b8; + cursor: pointer; + font-size: 0.7rem; + padding: 0 2px; + line-height: 1; + transition: color 0.15s; +} + +.anomalous-block-weight-btn:hover { + color: #fff; +} + +/* Floating Action Dock (Slides up only when blocks exist) */ +.anomalous-floating-action-dock { + position: absolute; + bottom: 8px; + left: 8px; + right: 8px; + height: 38px; + background: rgba(22, 25, 32, 0.94); + backdrop-filter: blur(16px); + border: 1px solid rgba(56, 189, 248, 0.35); + border-radius: 20px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.65), 0 0 12px rgba(56, 189, 248, 0.18); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; + z-index: 20; + animation: anmSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1); + box-sizing: border-box; +} + +.anomalous-floating-dock-left { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.72rem; + color: #94a3b8; + white-space: nowrap; +} + +.anomalous-floating-dock-right { + display: flex; + align-items: center; + gap: 5px; +} + +/* Independent Popout Prompt Inspector Modal */ +.anomalous-prompt-inspector-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + z-index: 999999; + display: flex; + align-items: center; + justify-content: center; + animation: anmFadeIn 0.2s ease; +} + +.anomalous-prompt-inspector-modal { + width: 780px; + max-width: 92vw; + height: 520px; + max-height: 88vh; + background: var(--amb-bg-panel, #181a20); + border: 1px solid var(--amb-border, rgba(255, 255, 255, 0.16)); + border-radius: 14px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.75), 0 0 1px rgba(255, 255, 255, 0.2); + display: flex; + flex-direction: column; + overflow: hidden; + animation: anmScaleUp 0.22s cubic-bezier(0.16, 1, 0.3, 1); +} + +.anomalous-inspector-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.anomalous-inspector-body { + flex: 1; + min-height: 0; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.anomalous-inspector-textarea { + flex: 1; + width: 100%; + box-sizing: border-box; + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #e2e8f0; + font-family: Consolas, Monaco, "Courier New", monospace; + font-size: 0.88rem; + line-height: 1.6; + padding: 12px; + resize: none; + outline: none; + transition: border-color 0.15s; +} + +.anomalous-inspector-textarea:focus { + border-color: #38bdf8; + box-shadow: 0 0 12px rgba(56, 189, 248, 0.25); +} + +.anomalous-inspector-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: rgba(255, 255, 255, 0.03); + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +@keyframes anmScaleUp { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +@keyframes anmSlideUp { + from { transform: translateY(12px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} +