From 8bc137882b83a4a46e1b5fc2b3d4f5a0d0c200ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:45:07 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 182 +++++++++++++++++++++++++++++++ web/static/i18n/en-US.json | 3 + web/static/i18n/zh-CN.json | 3 + web/static/js/workflows.js | 217 ++++++++++++++++++++++++++++++++++++- 4 files changed, 404 insertions(+), 1 deletion(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 5f66d516..abe8e3cc 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -31778,6 +31778,188 @@ html[data-theme="dark"] .form-group select { cursor: pointer; } +.workflow-tool-native-select { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.workflow-tool-select { + position: relative; + width: 100%; + min-width: 0; +} + +.workflow-tool-select-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + min-height: 38px; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 7px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.875rem; + font-family: inherit; + cursor: pointer; + text-align: left; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.workflow-tool-select-value { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflow-tool-select-caret { + flex: 0 0 auto; + color: var(--text-secondary); + font-size: 0.75rem; + line-height: 1; + transition: transform 0.2s ease; +} + +.workflow-tool-select.open .workflow-tool-select-caret { + transform: rotate(180deg); +} + +.workflow-tool-select-trigger:hover:not(:disabled) { + border-color: var(--accent-color); +} + +.workflow-tool-select.open .workflow-tool-select-trigger { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.workflow-tool-select.is-disabled .workflow-tool-select-trigger { + cursor: not-allowed; + opacity: 0.56; + background: var(--bg-secondary, #f5f6f8); +} + +.workflow-tool-select-dropdown { + display: none; + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: 300; + max-height: 280px; + overflow: hidden; + padding: 0; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: var(--shadow-lg); + flex-direction: column; +} + +.workflow-tool-select.open .workflow-tool-select-dropdown { + display: flex; +} + +.workflow-tool-select-search { + flex-shrink: 0; + padding: 8px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); +} + +.workflow-tool-select-search-input { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.8125rem; + background: var(--bg-primary); + color: var(--text-primary); + font-family: inherit; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.workflow-tool-select-search-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1); +} + +.workflow-tool-select-search-input::placeholder { + color: var(--text-muted); +} + +.workflow-tool-select-options { + flex: 1; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 4px; +} + +.workflow-tool-select-option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-width: 0; + padding: 8px 10px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-primary); + font-size: 0.8125rem; + font-family: inherit; + cursor: pointer; + text-align: left; +} + +.workflow-tool-select-option:hover { + background: var(--bg-secondary); +} + +.workflow-tool-select-option.is-selected { + color: var(--accent-color); + font-weight: 600; + background: rgba(0, 102, 255, 0.08); +} + +.workflow-tool-select-check { + flex: 0 0 14px; + width: 14px; + color: var(--accent-color); + opacity: 0; + font-size: 0.75rem; + line-height: 1; +} + +.workflow-tool-select-option.is-selected .workflow-tool-select-check { + opacity: 1; +} + +.workflow-tool-select-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflow-tool-select-empty { + padding: 10px 12px; + text-align: center; + color: var(--text-muted); + font-size: 0.8125rem; + line-height: 1.4; +} + @media (max-width: 1400px) { .workflow-page-content { grid-template-columns: clamp(180px, 14vw, 220px) minmax(0, 1fr); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 10102447..de87956f 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -3079,6 +3079,9 @@ "inputKeys": "Input variables", "mcpTool": "MCP tool", "selectTool": "Select a tool", + "searchTool": "Search tools…", + "noToolsFound": "No matching tools", + "noToolsAvailable": "No tools available", "toolDisabled": " (disabled)", "argumentsTemplate": "Arguments template", "argumentsStatic": "Tool arguments (JSON)", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index be69f7f2..9981f270 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -3067,6 +3067,9 @@ "inputKeys": "输入变量", "mcpTool": "MCP 工具", "selectTool": "请选择工具", + "searchTool": "搜索工具…", + "noToolsFound": "未找到匹配的工具", + "noToolsAvailable": "暂无可用工具", "toolDisabled": "(未启用)", "argumentsTemplate": "参数模板", "argumentsStatic": "工具参数(JSON)", diff --git a/web/static/js/workflows.js b/web/static/js/workflows.js index 39be860a..818dfa85 100644 --- a/web/static/js/workflows.js +++ b/web/static/js/workflows.js @@ -23,6 +23,9 @@ let selectedElement = null; let workflowToolOptions = []; let workflowToolsLoaded = false; + const WORKFLOW_TOOL_SELECT_ID = 'workflow-tool-name'; + let workflowToolSelectRegistry = null; + let workflowToolSelectDocBound = false; const KNOWN_NODE_LABELS = { start: ['开始', 'Start'], @@ -359,6 +362,217 @@ return workflowToolOptions; } + function workflowToolOptionLabel(tool) { + return tool.key + (tool.enabled ? '' : _t('workflows.config.toolDisabled')); + } + + function closeWorkflowToolSelect() { + const reg = workflowToolSelectRegistry; + if (!reg || !reg.wrapper) return; + reg.wrapper.classList.remove('open'); + if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false'); + if (reg.searchInput) reg.searchInput.value = ''; + } + + function createWorkflowToolOptionButton(value, label, selectedValue) { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'workflow-tool-select-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-value', value); + item.title = label; + if (value === selectedValue) { + item.classList.add('is-selected'); + item.setAttribute('aria-selected', 'true'); + } else { + item.setAttribute('aria-selected', 'false'); + } + const check = document.createElement('span'); + check.className = 'workflow-tool-select-check'; + check.setAttribute('aria-hidden', 'true'); + check.textContent = '✓'; + const labelEl = document.createElement('span'); + labelEl.className = 'workflow-tool-select-label'; + labelEl.textContent = label; + labelEl.title = label; + item.appendChild(check); + item.appendChild(labelEl); + return item; + } + + function renderWorkflowToolSelectOptions(reg, query) { + const { select, optionsList } = reg; + optionsList.innerHTML = ''; + const q = (query || '').trim().toLowerCase(); + let matchCount = 0; + + Array.prototype.forEach.call(select.options, (opt) => { + if (opt.value === '') { + if (!q) { + optionsList.appendChild(createWorkflowToolOptionButton(opt.value, opt.textContent || '', select.value)); + } + return; + } + const label = opt.textContent || opt.value; + if (q && !label.toLowerCase().includes(q) && !opt.value.toLowerCase().includes(q)) return; + matchCount += 1; + optionsList.appendChild(createWorkflowToolOptionButton(opt.value, label, select.value)); + }); + + if (matchCount === 0) { + const empty = document.createElement('div'); + empty.className = 'workflow-tool-select-empty'; + empty.textContent = q + ? _t('workflows.config.noToolsFound') + : _t('workflows.config.noToolsAvailable'); + optionsList.appendChild(empty); + } + } + + function ensureWorkflowToolSearchUi(reg) { + if (reg.searchInput && reg.optionsList) return; + const { dropdown } = reg; + dropdown.innerHTML = ''; + + const searchWrap = document.createElement('div'); + searchWrap.className = 'workflow-tool-select-search'; + const searchInput = document.createElement('input'); + searchInput.type = 'search'; + searchInput.className = 'workflow-tool-select-search-input'; + searchInput.setAttribute('autocomplete', 'off'); + searchInput.setAttribute('data-i18n', 'workflows.config.searchTool'); + searchInput.setAttribute('data-i18n-attr', 'placeholder'); + searchInput.placeholder = _t('workflows.config.searchTool'); + searchWrap.appendChild(searchInput); + dropdown.appendChild(searchWrap); + reg.searchInput = searchInput; + + const optionsList = document.createElement('div'); + optionsList.className = 'workflow-tool-select-options'; + dropdown.appendChild(optionsList); + reg.optionsList = optionsList; + + searchInput.addEventListener('input', () => renderWorkflowToolSelectOptions(reg, searchInput.value)); + searchInput.addEventListener('click', (e) => e.stopPropagation()); + searchInput.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'Escape') closeWorkflowToolSelect(); + }); + } + + function syncWorkflowToolSelect() { + const select = document.getElementById(WORKFLOW_TOOL_SELECT_ID); + const reg = workflowToolSelectRegistry; + if (!select || !reg || reg.select !== select) return; + const selected = select.options[select.selectedIndex]; + reg.valueSpan.textContent = selected && selected.value + ? selected.textContent + : _t('workflows.config.selectTool'); + if (reg.optionsList) { + renderWorkflowToolSelectOptions(reg, reg.searchInput ? reg.searchInput.value : ''); + } + reg.trigger.disabled = !!select.disabled; + reg.wrapper.classList.toggle('is-disabled', !!select.disabled); + } + + function enhanceWorkflowToolSelect() { + const select = document.getElementById(WORKFLOW_TOOL_SELECT_ID); + if (!select) { + workflowToolSelectRegistry = null; + return; + } + if (select.dataset.workflowToolCustom === '1' && workflowToolSelectRegistry && workflowToolSelectRegistry.select === select) { + syncWorkflowToolSelect(); + return; + } + workflowToolSelectRegistry = null; + + select.dataset.workflowToolCustom = '1'; + select.classList.add('workflow-tool-native-select'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + const wrapper = document.createElement('div'); + wrapper.className = 'workflow-tool-select'; + + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'workflow-tool-select-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + const valueSpan = document.createElement('span'); + valueSpan.className = 'workflow-tool-select-value'; + trigger.appendChild(valueSpan); + const caret = document.createElement('span'); + caret.className = 'workflow-tool-select-caret'; + caret.setAttribute('aria-hidden', 'true'); + caret.textContent = '▾'; + trigger.appendChild(caret); + + const dropdown = document.createElement('div'); + dropdown.className = 'workflow-tool-select-dropdown'; + dropdown.setAttribute('role', 'listbox'); + + const parent = select.parentNode; + parent.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(dropdown); + wrapper.appendChild(select); + + workflowToolSelectRegistry = { + wrapper, + trigger, + dropdown, + select, + valueSpan, + searchInput: null, + optionsList: null + }; + + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + if (select.disabled) return; + const open = wrapper.classList.contains('open'); + closeWorkflowToolSelect(); + if (!open) { + wrapper.classList.add('open'); + trigger.setAttribute('aria-expanded', 'true'); + ensureWorkflowToolSearchUi(workflowToolSelectRegistry); + if (workflowToolSelectRegistry.searchInput) { + workflowToolSelectRegistry.searchInput.value = ''; + renderWorkflowToolSelectOptions(workflowToolSelectRegistry, ''); + requestAnimationFrame(() => workflowToolSelectRegistry.searchInput.focus()); + } + } + }); + + dropdown.addEventListener('click', (e) => { + const opt = e.target.closest('.workflow-tool-select-option'); + if (!opt) return; + e.stopPropagation(); + const val = opt.getAttribute('data-value'); + if (val === null) return; + if (select.value !== val) { + select.value = val; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + closeWorkflowToolSelect(); + syncWorkflowToolSelect(); + }); + + select.addEventListener('change', () => syncWorkflowToolSelect()); + + if (!workflowToolSelectDocBound) { + workflowToolSelectDocBound = true; + document.addEventListener('click', closeWorkflowToolSelect); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeWorkflowToolSelect(); + }); + } + + syncWorkflowToolSelect(); + } + function readWorkflowMetaFromForm() { const idEl = document.getElementById('workflow-id'); const nameEl = document.getElementById('workflow-name'); @@ -644,7 +858,7 @@ wrap.innerHTML = ` ${joinStrategyHtml(cfg)}