diff --git a/docs/plans/code-structure-refactor.md b/docs/plans/code-structure-refactor.md index 4d8fc29..8011c92 100644 --- a/docs/plans/code-structure-refactor.md +++ b/docs/plans/code-structure-refactor.md @@ -83,7 +83,7 @@ 在阶段 1 稳定后,从 `createDOM()` 提取: - `ui_settings_hub.js`:设置中心与模型显示设置。接口覆盖 open/close/refreshLanguage,保留原来的设置来源、键名和生效时机。 -- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`shortcut_layout.js`、`sidebar_actions.js`、`ui_shortcut_organizer.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。 +- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`sidebar_actions.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。 - `ui_browser_navigation.js`:主面板切换与返回协调。先收拢现有 `hideAllPanels/closeWorkspace` 相关调用,不引入新的路由系统;笔记模块最终不再独占其他工作台的公共关闭逻辑。 - 窗口拖动、缩放、停靠先保留在外壳;仍形成独立大段时再提取 `browser_window.js`,复用生命周期工具,不改变尺寸偏好。 diff --git a/docs/plans/customizable-tool-shortcuts.md b/docs/plans/customizable-tool-shortcuts.md index 159aa1d..a222066 100644 --- a/docs/plans/customizable-tool-shortcuts.md +++ b/docs/plans/customizable-tool-shortcuts.md @@ -1,6 +1,6 @@ # 工具箱与快捷栏自定义:交给 Gemini 的实施计划 -状态:待实施的产品与工程计划,不代表当前版本已经具备以下功能。 +状态:已放弃。当前是固定快捷栏(见 `ui_toolbox.js`);按本计划写的 `shortcut_layout.js`、`ui_shortcut_organizer.js` 从未接入,已删除。只作历史参考,不要照此实施。 基线:`77ae7ce88`;开工时以实际 Git 差异和现有代码为准,不回退后续工作。 ## 目标与范围 diff --git a/web/modules/locales.js b/web/modules/locales.js index b907b47..cbad8ee 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -77,17 +77,6 @@ export const i18n = { versionError_unknown_tag: "这不是已发布的版本。", versionError_no_previous: "没有可以撤销的切换。", versionError_no_default_branch: "无法确定默认分支。", - shortcutBarPinned: "已固定", - shortcutBarPin: "固定到快捷栏", - shortcutBarUnpin: "从快捷栏移除", - shortcutBarMoveLeft: "向左移动", - shortcutBarMoveRight: "向右移动", - shortcutBarCapacityFull: "快捷栏已满,请先将一个工具移回工具箱", - shortcutBarReset: "恢复默认快捷栏", - shortcutBarResetConfirm: "确定要将快捷栏恢复为默认布局吗?", - shortcutDropHint: "拖到下方,固定常用工具;拖回这里,收起快捷入口", - shortcutDropRelease: "松开,从快捷栏移除", - shortcutBarAnchorTip: "固定入口,不可移动", updateGuideStartTour: "🎯 界面按键遮罩导览", updateGuideTourBanner: "💡 想在主界面实地体验?点击开启按键遮罩导览 ›", materialDragGlobalHint: "💡 提示:按住卡片直接拖到画布节点上注入参数,拖到空白处载入工作流", @@ -1810,17 +1799,6 @@ export const i18n = { versionError_unknown_tag: "That is not a published version.", versionError_no_previous: "There is no switch to undo.", versionError_no_default_branch: "Couldn't determine the default branch.", - shortcutBarPinned: "Pinned", - shortcutBarPin: "Pin to shortcuts", - shortcutBarUnpin: "Remove from shortcuts", - shortcutBarMoveLeft: "Move left", - shortcutBarMoveRight: "Move right", - shortcutBarCapacityFull: "Shortcut bar is full. Move a tool back to Toolbox first", - shortcutBarReset: "Reset shortcut bar", - shortcutBarResetConfirm: "Reset the shortcut bar to default layout?", - shortcutDropHint: "Drag down to pin shortcuts; drag back here to unpin", - shortcutDropRelease: "Release to remove from shortcuts", - shortcutBarAnchorTip: "Fixed anchor, cannot be moved", updateGuideStartTour: "🎯 Spotlight Tour", updateGuideTourBanner: "💡 Prefer to see them in action? Start Spotlight Tour ›", materialDragGlobalHint: "💡 Tip: Drag cards onto canvas nodes to inject values, or onto empty canvas to load the workflow", diff --git a/web/modules/shortcut_layout.js b/web/modules/shortcut_layout.js deleted file mode 100644 index 4207102..0000000 --- a/web/modules/shortcut_layout.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * shortcut_layout.js - State and Persistence for Customizable Shortcut Bar - * - * Implements schema validation, capacity limits, pinning, unpinning, - * reordering and reset logic with localStorage persistence. - */ - -import { CATALOG_TOOLS } from './tool_registry.js'; - -export const SHORTCUT_STORAGE_KEY = 'anomalous_shortcut_layout_v1'; -export const SHORTCUT_SCHEMA_VERSION = 1; -export const MAX_PINNED_SHORTCUTS = 4; - -export const DEFAULT_PINNED_SHORTCUTS = Object.freeze([ - 'scan', - 'doctor', - 'assistant', - 'materials', -]); - -// In-memory fallback if localStorage fails or is blocked -let sessionMemoryLayout = null; - -const VALID_CATALOG_IDS = new Set(CATALOG_TOOLS.map(t => t.id)); - -export function isValidCatalogToolId(id) { - return VALID_CATALOG_IDS.has(id); -} - -/** - * Normalizes an array of tool IDs: - * - Drops invalid IDs, anchor IDs and non-strings - * - Deduplicates preserving first occurrence - * - Truncates to MAX_PINNED_SHORTCUTS - */ -export function normalizePinnedIds(ids) { - if (!Array.isArray(ids)) return [...DEFAULT_PINNED_SHORTCUTS]; - const seen = new Set(); - const result = []; - for (const id of ids) { - if (typeof id === 'string' && VALID_CATALOG_IDS.has(id) && !seen.has(id)) { - seen.add(id); - result.push(id); - if (result.length >= MAX_PINNED_SHORTCUTS) break; - } - } - return result; -} - -/** - * Load shortcut layout from storage. - * Explicit empty array is valid and preserved. - */ -export function loadShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - if (sessionMemoryLayout !== null && !storage) { - return [...sessionMemoryLayout]; - } - - if (!storage) { - return sessionMemoryLayout !== null ? [...sessionMemoryLayout] : [...DEFAULT_PINNED_SHORTCUTS]; - } - - try { - const raw = storage.getItem(SHORTCUT_STORAGE_KEY); - if (raw === null || raw === undefined) { - return [...DEFAULT_PINNED_SHORTCUTS]; - } - const parsed = JSON.parse(raw); - if (!parsed || parsed.schemaVersion !== SHORTCUT_SCHEMA_VERSION || !Array.isArray(parsed.pinned)) { - return [...DEFAULT_PINNED_SHORTCUTS]; - } - return normalizePinnedIds(parsed.pinned); - } catch (err) { - console.warn('[AMB] Failed to read shortcut layout from storage, resetting to defaults:', err); - sessionMemoryLayout = [...DEFAULT_PINNED_SHORTCUTS]; - return [...DEFAULT_PINNED_SHORTCUTS]; - } -} - -/** - * Persist shortcut layout to storage and memory. - */ -export function saveShortcutLayout(pinnedIds, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - const normalized = normalizePinnedIds(pinnedIds); - sessionMemoryLayout = [...normalized]; - - if (!storage) { - return { success: true, layout: normalized, memoryOnly: true }; - } - - try { - const payload = JSON.stringify({ - schemaVersion: SHORTCUT_SCHEMA_VERSION, - pinned: normalized, - }); - storage.setItem(SHORTCUT_STORAGE_KEY, payload); - return { success: true, layout: normalized, memoryOnly: false }; - } catch (err) { - console.warn('[AMB] Failed to write shortcut layout to storage, saved to session memory:', err); - return { success: true, layout: normalized, memoryOnly: true, error: err }; - } -} - -/** - * Pin a tool to the shortcut bar. - * If already pinned, reorder to targetIndex. - * If not pinned and at capacity, rejects with 'capacity_full'. - */ -export function pinTool(toolId, targetIndex = -1, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - if (!isValidCatalogToolId(toolId)) { - return { success: false, reason: 'unknown_tool', layout: loadShortcutLayout(storage) }; - } - - const current = loadShortcutLayout(storage); - const existingIndex = current.indexOf(toolId); - - if (existingIndex !== -1) { - // Already pinned: reorder to target position - current.splice(existingIndex, 1); - const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length; - current.splice(insertAt, 0, toolId); - saveShortcutLayout(current, storage); - return { success: true, reordered: true, layout: current }; - } - - if (current.length >= MAX_PINNED_SHORTCUTS) { - return { success: false, reason: 'capacity_full', layout: current }; - } - - const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length; - current.splice(insertAt, 0, toolId); - saveShortcutLayout(current, storage); - return { success: true, reordered: false, layout: current }; -} - -/** - * Unpin a tool from the shortcut bar (returns it to toolbox only). - */ -export function unpinTool(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - const current = loadShortcutLayout(storage); - const index = current.indexOf(toolId); - if (index === -1) { - return { success: true, changed: false, layout: current }; - } - current.splice(index, 1); - saveShortcutLayout(current, storage); - return { success: true, changed: true, layout: current }; -} - -/** - * Reorder an item in the shortcut bar. - */ -export function reorderShortcut(fromIndex, toIndex, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - const current = loadShortcutLayout(storage); - if (fromIndex < 0 || fromIndex >= current.length || toIndex < 0 || toIndex >= current.length || fromIndex === toIndex) { - return { success: false, layout: current }; - } - const [item] = current.splice(fromIndex, 1); - current.splice(toIndex, 0, item); - saveShortcutLayout(current, storage); - return { success: true, layout: current }; -} - -/** - * Reset shortcut layout to defaults. - */ -export function resetShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - saveShortcutLayout(DEFAULT_PINNED_SHORTCUTS, storage); - return { success: true, layout: [...DEFAULT_PINNED_SHORTCUTS] }; -} - -/** - * Check if a tool is currently pinned. - */ -export function isToolPinned(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) { - const current = loadShortcutLayout(storage); - return current.includes(toolId); -} diff --git a/web/modules/ui_shortcut_organizer.js b/web/modules/ui_shortcut_organizer.js deleted file mode 100644 index 49734c7..0000000 --- a/web/modules/ui_shortcut_organizer.js +++ /dev/null @@ -1,503 +0,0 @@ -/** - * ui_shortcut_organizer.js - Drag-and-drop & Menu Management for Shortcut Bar & Toolbox - * - * Implements: - * 1. Pointer drag-and-drop between Toolbox and Shortcut Bar (with 6px deadzone). - * 2. 350ms auto-expand when dragging over closed Toolbox button. - * 3. Drop indicators and visual drop-zones. - * 4. Capacity limit feedback (max 4 pinned items). - * 5. Accessible context / "More" menus for organizing without mouse drag. - * 6. Full cancel/cleanup on Escape, pointercancel, or window blur. - */ - -import { translate as t } from './locales.js'; -import { getToolDefinition, CATALOG_TOOLS, FIXED_ANCHORS } from './tool_registry.js'; -import { - loadShortcutLayout, - pinTool, - unpinTool, - reorderShortcut, - resetShortcutLayout, - isToolPinned, - MAX_PINNED_SHORTCUTS, -} from './shortcut_layout.js'; -import { setDraggingState, hideTooltipImmediately } from './sidebar_actions.js'; -import { showWorkbenchToast } from './ui_prompt_toast.js'; - -let activeDrag = null; -let autoOpenTimer = null; -let activeMenuEl = null; - -function closeActiveMenu() { - if (activeMenuEl) { - activeMenuEl.remove(); - activeMenuEl = null; - } -} - -document.addEventListener('mousedown', (e) => { - if (activeMenuEl && !activeMenuEl.contains(e.target)) { - closeActiveMenu(); - } -}); - -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - if (activeDrag) { - cancelCurrentDrag(); - } - closeActiveMenu(); - } -}); - -export function isOrganizingDragActive() { - return Boolean(activeDrag && activeDrag.isDragging); -} - -function showCapacityFullToast() { - showWorkbenchToast(t('shortcutBarCapacityFull')); -} - -/** - * Creates and shows a context dropdown menu. - */ -function showDropdownMenu(items, x, y) { - closeActiveMenu(); - if (!items || items.length === 0) return; - - const menu = document.createElement('div'); - menu.className = 'anomalous-shortcut-context-menu'; - menu.style.position = 'fixed'; - menu.style.zIndex = '1000002'; - menu.style.display = 'flex'; - menu.style.flexDirection = 'column'; - menu.style.minWidth = '140px'; - menu.style.padding = '4px'; - menu.style.borderRadius = '8px'; - menu.style.background = 'rgba(20, 22, 28, 0.96)'; - menu.style.backdropFilter = 'blur(16px)'; - menu.style.border = '1px solid rgba(255, 255, 255, 0.14)'; - menu.style.boxShadow = '0 12px 36px rgba(0, 0, 0, 0.7), 0 0 1px 1px rgba(255, 255, 255, 0.1)'; - - for (const item of items) { - if (item.separator) { - const sep = document.createElement('div'); - sep.style.height = '1px'; - sep.style.margin = '4px 2px'; - sep.style.background = 'rgba(255, 255, 255, 0.08)'; - menu.appendChild(sep); - continue; - } - - const btn = document.createElement('button'); - btn.type = 'button'; - btn.textContent = item.label; - btn.disabled = Boolean(item.disabled); - btn.style.display = 'flex'; - btn.style.alignItems = 'center'; - btn.style.gap = '8px'; - btn.style.width = '100%'; - btn.style.padding = '6px 10px'; - btn.style.border = 'none'; - btn.style.borderRadius = '5px'; - btn.style.background = 'transparent'; - btn.style.color = item.disabled ? '#64748b' : (item.danger ? '#f87171' : '#e2e8f0'); - btn.style.fontSize = '12px'; - btn.style.cursor = item.disabled ? 'not-allowed' : 'pointer'; - btn.style.textAlign = 'left'; - btn.style.transition = 'all 0.12s ease'; - - if (!item.disabled) { - btn.onmouseover = () => { - btn.style.background = 'rgba(255, 255, 255, 0.08)'; - btn.style.color = '#ffffff'; - }; - btn.onmouseout = () => { - btn.style.background = 'transparent'; - btn.style.color = item.danger ? '#f87171' : '#e2e8f0'; - }; - btn.onclick = (e) => { - e.stopPropagation(); - closeActiveMenu(); - item.action(); - }; - } - - menu.appendChild(btn); - } - - document.body.appendChild(menu); - - // Bounding clamp - const menuRect = menu.getBoundingClientRect(); - let left = x; - let top = y; - - if (left + menuRect.width > window.innerWidth - 8) { - left = window.innerWidth - menuRect.width - 8; - } - if (left < 8) left = 8; - - if (top + menuRect.height > window.innerHeight - 8) { - top = y - menuRect.height; - } - if (top < 8) top = 8; - - menu.style.left = `${Math.round(left)}px`; - menu.style.top = `${Math.round(top)}px`; - - activeMenuEl = menu; -} - -export function openToolboxCardMenu(toolId, targetEl, onLayoutChange) { - if (!targetEl || !toolId) return; - const isPinned = isToolPinned(toolId); - const layout = loadShortcutLayout(); - const isFull = layout.length >= MAX_PINNED_SHORTCUTS; - - const items = []; - if (isPinned) { - items.push({ - label: t('shortcutBarUnpin'), - action: () => { - unpinTool(toolId); - onLayoutChange?.(); - } - }); - } else { - items.push({ - label: isFull ? `${t('shortcutBarPin')} (${t('shortcutBarCapacityFull') ? '已满' : 'Full'})` : t('shortcutBarPin'), - disabled: isFull, - action: () => { - const res = pinTool(toolId); - if (!res.success && res.reason === 'capacity_full') { - showCapacityFullToast(); - } else { - onLayoutChange?.(); - } - } - }); - } - - const rect = targetEl.getBoundingClientRect(); - showDropdownMenu(items, rect.left, rect.bottom + 4); -} - -export function openShortcutButtonMenu(toolId, buttonEl, onLayoutChange) { - if (!buttonEl || !toolId) return; - const layout = loadShortcutLayout(); - const idx = layout.indexOf(toolId); - if (idx === -1) return; - - const items = [ - { - label: t('shortcutBarMoveLeft'), - disabled: idx <= 0, - action: () => { - reorderShortcut(idx, idx - 1); - onLayoutChange?.(); - } - }, - { - label: t('shortcutBarMoveRight'), - disabled: idx >= layout.length - 1, - action: () => { - reorderShortcut(idx, idx + 1); - onLayoutChange?.(); - } - }, - { separator: true }, - { - label: t('shortcutBarUnpin'), - danger: true, - action: () => { - unpinTool(toolId); - onLayoutChange?.(); - } - } - ]; - - const rect = buttonEl.getBoundingClientRect(); - showDropdownMenu(items, rect.left, rect.top - 4); -} - -/** - * Cancel and clean up active drag operation - */ -function cancelCurrentDrag() { - if (autoOpenTimer) { - clearTimeout(autoOpenTimer); - autoOpenTimer = null; - } - if (activeDrag) { - if (activeDrag.ghostEl) { - activeDrag.ghostEl.remove(); - } - if (activeDrag.indicatorEl) { - activeDrag.indicatorEl.remove(); - } - if (activeDrag.dropzoneEl) { - activeDrag.dropzoneEl.classList.remove('is-drag-over'); - const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text'); - if (txt) txt.textContent = t('shortcutDropHint'); - } - if (activeDrag.originEl) { - activeDrag.originEl.style.opacity = '1'; - } - } - setDraggingState(false); - activeDrag = null; -} - -/** - * Binds pointer drag & drop to a shortcut bar button or toolbox card. - */ -export function bindDraggableTool(element, { - toolId, - source, // 'shortcut' | 'toolbox' - toolboxModal, - toolboxBtn, - sidebarActionsEl, - onLayoutChange, - onToolClick, -}) { - if (!element || !toolId) return; - - let startX = 0; - let startY = 0; - let hasMoved = false; - - const onPointerDown = (e) => { - // Only primary mouse button (or touch/pen) - if (e.button !== undefined && e.button !== 0 && e.pointerType === 'mouse') return; - // Ignore if clicking a "More" menu button inside card - if (e.target?.closest?.('.anomalous-tool-more-btn')) return; - - startX = e.clientX; - startY = e.clientY; - hasMoved = false; - - const spec = getToolDefinition(toolId); - if (!spec) return; - - const onPointerMove = (moveEvent) => { - const dx = moveEvent.clientX - startX; - const dy = moveEvent.clientY - startY; - const dist = Math.hypot(dx, dy); - - if (!hasMoved) { - if (dist > 6) { - hasMoved = true; - setDraggingState(true); - hideTooltipImmediately(); - closeActiveMenu(); - - // Create ghost element - const ghost = document.createElement('div'); - ghost.className = 'anomalous-shortcut-drag-ghost'; - ghost.innerHTML = ` -
-