From 622390d0561c66f0fec38d787670ba053245d01a Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Thu, 9 Apr 2026 21:18:27 +0800 Subject: [PATCH] feat: redesign regex editor as single column --- style.css | 133 ++++++++++++++++- ui/panel.js | 403 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 454 insertions(+), 82 deletions(-) diff --git a/style.css b/style.css index 8569025..c03944d 100644 --- a/style.css +++ b/style.css @@ -2694,12 +2694,6 @@ top: auto; } -.bme-task-regex-top { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - .bme-task-block-count { font-size: 11px; color: var(--bme-on-surface-dim); @@ -3063,6 +3057,126 @@ margin-top: 8px; } +/* ═══════ Single-column Regex Editor ═══════ */ + +.bme-regex-settings-stack { + display: flex; + flex-direction: column; + gap: 14px; +} + +.bme-regex-settings-card .bme-task-section-label { + margin-top: 0; + margin-bottom: 10px; +} + +.bme-regex-rule-card .bme-task-empty, +.bme-regex-rule-card .bme-task-note { + margin: 0; +} + +.bme-regex-rule-rows { + display: flex; + flex-direction: column; + gap: 4px; +} + +.bme-regex-rule-row { + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + background: rgba(255, 255, 255, 0.025); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.bme-regex-rule-row:hover { + border-color: rgba(255, 255, 255, 0.12); +} + +.bme-regex-rule-row.is-expanded { + border-color: var(--bme-primary); + box-shadow: 0 0 0 1px var(--bme-primary); +} + +.bme-regex-rule-row.is-disabled { + opacity: 0.56; +} + +.bme-regex-rule-row.dragging { + opacity: 0.35; +} + +.bme-regex-rule-row.drag-over-top { + border-top: 2px solid var(--bme-primary); + margin-top: -1px; +} + +.bme-regex-rule-row.drag-over-bottom { + border-bottom: 2px solid var(--bme-primary); + margin-bottom: -1px; +} + +.bme-regex-rule-row-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + min-height: 44px; + cursor: pointer; + user-select: none; + flex-wrap: wrap; +} + +.bme-regex-rule-name { + flex: 0 1 220px; + min-width: 120px; + font-size: 13px; + font-weight: 600; + color: var(--bme-on-surface); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bme-regex-rule-status { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 42px; + padding: 2px 7px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + white-space: nowrap; + flex-shrink: 0; +} + +.bme-regex-rule-status.is-enabled { + background: rgba(80, 180, 120, 0.18); + color: #80d0a0; +} + +.bme-regex-rule-status.is-disabled { + background: rgba(160, 160, 180, 0.14); + color: var(--bme-on-surface-dim); +} + +.bme-regex-rule-preview { + flex: 1 1 260px; + min-width: 0; + font-size: 11px; + line-height: 1.45; + color: var(--bme-on-surface-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bme-regex-rule-expand { + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding: 12px; + animation: bme-block-expand-in 0.2s ease; +} + @keyframes bme-block-expand-in { from { opacity: 0; @@ -3934,7 +4048,6 @@ .bme-theme-card-grid, .bme-task-field-grid, .bme-task-expand-row2, - .bme-task-regex-top, .bme-task-debug-grid { grid-template-columns: 1fr; } @@ -4131,6 +4244,12 @@ max-width: 100%; } + .bme-regex-rule-preview { + flex: 1 1 100%; + order: 10; + margin-left: 28px; + } + /* Dashboard 统计卡片横向滚动 */ .bme-stats-grid { display: flex; diff --git a/ui/panel.js b/ui/panel.js index 4c91ffc..4a48d62 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -282,6 +282,8 @@ let currentTaskProfileTabId = "generation"; let currentTaskProfileBlockId = ""; let currentTaskProfileDragBlockId = ""; let currentTaskProfileRuleId = ""; +let currentTaskProfileDragRuleId = ""; +let currentTaskProfileDragRuleIsGlobal = false; let showGlobalRegexPanel = false; let currentGlobalRegexRuleId = ""; let currentCognitionOwnerKey = ""; @@ -5060,6 +5062,74 @@ function _bindTaskProfileWorkspace() { currentTaskProfileDragBlockId = ""; _clearTaskBlockDragIndicators(workspace); }); + workspace.addEventListener("dragstart", (event) => { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + const handle = target.closest(".bme-regex-drag-handle"); + const row = target.closest(".bme-regex-rule-row"); + if (!handle || !(row instanceof HTMLElement)) return; + const ruleId = String(row.dataset.ruleId || "").trim(); + if (!ruleId) return; + currentTaskProfileDragRuleId = ruleId; + currentTaskProfileDragRuleIsGlobal = _isGlobalRegexPanelTarget(row); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.dropEffect = "move"; + event.dataTransfer.setData("text/plain", ruleId); + } + window.requestAnimationFrame(() => { + row.classList.add("dragging"); + }); + }); + workspace.addEventListener("dragover", (event) => { + const target = event.target; + if (!(target instanceof HTMLElement) || !currentTaskProfileDragRuleId) return; + const row = target.closest(".bme-regex-rule-row"); + if (!(row instanceof HTMLElement)) return; + const isGlobalRow = _isGlobalRegexPanelTarget(row); + if (isGlobalRow !== currentTaskProfileDragRuleIsGlobal) return; + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = "move"; + } + const position = _getRegexRuleDropPosition(row, event.clientY); + _setRegexRuleDragIndicator(workspace, row, position); + }); + workspace.addEventListener("dragleave", (event) => { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + const row = target.closest(".bme-regex-rule-row"); + if (!(row instanceof HTMLElement)) return; + const relatedTarget = event.relatedTarget; + if (relatedTarget instanceof Node && row.contains(relatedTarget)) { + return; + } + row.classList.remove("drag-over-top", "drag-over-bottom"); + }); + workspace.addEventListener("drop", (event) => { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + const row = target.closest(".bme-regex-rule-row"); + if (!(row instanceof HTMLElement)) return; + const isGlobalRow = _isGlobalRegexPanelTarget(row); + if (isGlobalRow !== currentTaskProfileDragRuleIsGlobal) return; + event.preventDefault(); + const sourceId = + currentTaskProfileDragRuleId || + String(event.dataTransfer?.getData("text/plain") || "").trim(); + const targetId = String(row.dataset.ruleId || "").trim(); + const position = _getRegexRuleDropPosition(row, event.clientY); + _clearRegexRuleDragIndicators(workspace); + currentTaskProfileDragRuleId = ""; + currentTaskProfileDragRuleIsGlobal = false; + if (!sourceId || !targetId || sourceId === targetId) return; + _reorderRegexRules(sourceId, targetId, position, isGlobalRow); + }); + workspace.addEventListener("dragend", () => { + currentTaskProfileDragRuleId = ""; + currentTaskProfileDragRuleIsGlobal = false; + _clearRegexRuleDragIndicators(workspace); + }); workspace.dataset.bmeBound = "true"; } @@ -5280,6 +5350,13 @@ function _handleTaskProfileWorkspaceInput(event) { } else { _persistSelectedRegexRuleField(target, false); } + return; + } + + if (target.matches("[data-regex-rule-row-enabled]")) { + const ruleId = String(target.dataset.ruleId || "").trim(); + if (!ruleId) return; + _persistRegexRuleEnabledById(ruleId, Boolean(target.checked), isGlobalRegexPanel, false); } } @@ -5353,6 +5430,13 @@ function _handleTaskProfileWorkspaceChange(event) { } else { _persistSelectedRegexRuleField(target, true); } + return; + } + + if (target.matches("[data-regex-rule-row-enabled]")) { + const ruleId = String(target.dataset.ruleId || "").trim(); + if (!ruleId) return; + _persistRegexRuleEnabledById(ruleId, Boolean(target.checked), isGlobalRegexPanel, true); } } @@ -5392,10 +5476,10 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) { if (currentTaskProfileBlockId && !blocks.some((block) => block.id === currentTaskProfileBlockId)) { currentTaskProfileBlockId = blocks[0]?.id || ""; } - if (!regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) { + if (currentTaskProfileRuleId && !regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) { currentTaskProfileRuleId = regexRules[0]?.id || ""; } - if (!globalRegexRules.some((rule) => rule.id === currentGlobalRegexRuleId)) { + if (currentGlobalRegexRuleId && !globalRegexRules.some((rule) => rule.id === currentGlobalRegexRuleId)) { currentGlobalRegexRuleId = globalRegexRules[0]?.id || ""; } @@ -6112,6 +6196,26 @@ async function _handleTaskProfileWorkspaceClick(event) { _refreshTaskProfileWorkspace(); return; } + case "toggle-regex-rule-expand": { + const originEl = event.target; + if ( + originEl.closest(".bme-task-row-toggle") || + originEl.closest(".bme-task-row-btn-danger") || + originEl.closest(".bme-regex-drag-handle") + ) { + return; + } + const ruleId = actionEl.dataset.ruleId || ""; + if (_isGlobalRegexPanelTarget(actionEl)) { + currentGlobalRegexRuleId = + currentGlobalRegexRuleId === ruleId ? "" : ruleId; + } else { + currentTaskProfileRuleId = + currentTaskProfileRuleId === ruleId ? "" : ruleId; + } + _refreshTaskProfileWorkspace(); + return; + } case "select-regex-rule": if (_isGlobalRegexPanelTarget(actionEl)) { currentGlobalRegexRuleId = actionEl.dataset.ruleId || ""; @@ -6560,7 +6664,6 @@ function _renderTaskRegexTab(state, options = {}) { const selectedRule = options.selectedRule === undefined ? state.selectedRule : options.selectedRule; const normalizedStages = normalizeTaskRegexStages(regex.stages || {}); - const selectAction = options.selectAction || "select-regex-rule"; const deleteAction = options.deleteAction || "delete-regex-rule"; const addAction = options.addAction || "add-regex-rule"; const addButtonLabel = options.addButtonLabel || "+ 新增规则"; @@ -6578,6 +6681,9 @@ function _renderTaskRegexTab(state, options = {}) { const emptyText = options.emptyText || "当前预设还没有本地正则规则。"; const defaultNamePrefix = options.defaultNamePrefix || "本地规则"; const headerExtraActions = options.extraHeaderActions || ""; + const enableToggleTitle = options.enableToggleTitle || "启用任务正则"; + const enableToggleDesc = + options.enableToggleDesc || "关闭后当前配置不执行任何任务级正则。"; const editorState = { ...state, selectedRule, @@ -6585,8 +6691,8 @@ function _renderTaskRegexTab(state, options = {}) { return `
-
-
+
+
${_escHtml(sectionTitle)}
@@ -6605,8 +6711,8 @@ function _renderTaskRegexTab(state, options = {}) {
+
+
${[ @@ -6652,7 +6760,9 @@ function _renderTaskRegexTab(state, options = {}) { ) .join("")}
+
+
${TASK_PROFILE_REGEX_STAGES.map( @@ -6672,42 +6782,37 @@ function _renderTaskRegexTab(state, options = {}) { ).join("")}
- -
-
-
-
${_escHtml(rulesTitle)}
-
- ${_escHtml(rulesSubtitle)} -
-
- -
- -
- ${regexRules.length - ? regexRules - .map((rule, index) => - _renderRegexRuleListItem(rule, index, editorState, { - selectAction, - deleteAction, - defaultNamePrefix, - }) - ) - .join("") - : ` -
- ${_escHtml(emptyText)} -
- `} -
-
-
- ${_renderRegexRuleEditor(editorState)} +
+
+
+
${_escHtml(rulesTitle)}
+
+ ${_escHtml(rulesSubtitle)} +
+
+ +
+ +
+ ${regexRules.length + ? regexRules + .map((rule, index) => + _renderRegexRuleRow(rule, index, editorState, { + deleteAction, + defaultNamePrefix, + }) + ) + .join("") + : ` +
+ ${_escHtml(emptyText)} +
+ `} +
`; @@ -6730,6 +6835,8 @@ function _renderGlobalRegexPanel(state) { wrapperClassName: "bme-global-regex-panel", sectionTitle: "通用正则设置", sectionSubtitle: "所有任务共享同一套任务正则开关、复用来源、执行阶段与附加规则。", + enableToggleTitle: "启用通用正则", + enableToggleDesc: "关闭后所有任务都不执行任何共享正则配置。", rulesTitle: "通用附加规则", rulesSubtitle: "这里维护所有任务共享的附加规则。", emptyText: "当前还没有通用正则规则。", @@ -7825,63 +7932,94 @@ function _renderGenerationField(field, value, state = {}) { `; } -function _renderRegexRuleListItem(rule, index, state, options = {}) { - const isSelected = rule.id === state.selectedRule?.id; - const selectAction = options.selectAction || "select-regex-rule"; +function _formatRegexRulePreview(findRegex = "") { + const collapsed = String(findRegex || "") + .replace(/\s+/g, " ") + .trim(); + return collapsed || "(未填写 find_regex)"; +} + +function _renderRegexRuleRow(rule, index, state, options = {}) { + const isExpanded = rule.id === state.selectedRule?.id; const deleteAction = options.deleteAction || "delete-regex-rule"; const defaultNamePrefix = options.defaultNamePrefix || "本地规则"; + const statusLabel = rule.enabled ? "启用" : "停用"; + const previewText = _formatRegexRulePreview(rule.find_regex); return ` -
- -
+ +
+ ${isExpanded + ? ` +
+ ${_renderRegexRuleInlineEditor(rule)} +
+ ` + : ""}
`; } -function _renderRegexRuleEditor(state) { - const rule = state.selectedRule; - if (!rule) { - return ` -
规则详情
-
从左侧规则列表选择一条规则进行编辑。
- `; - } +function _renderRegexRuleInlineEditor(rule) { const trimStrings = Array.isArray(rule.trim_strings) ? rule.trim_strings.join("\n") : String(rule.trim_strings || ""); return ` -
-
-
规则详情
-
- 字段尽量与 Tavern 正则结构保持对齐,方便后续导入导出与对照。 -
-
- ${rule.enabled ? "启用中" : "已停用"} +
+ 字段尽量与 Tavern 正则结构保持对齐,方便后续导入导出与对照。
@@ -8005,6 +8143,17 @@ function _renderRegexRuleEditor(state) { />
+ + `; } @@ -8111,6 +8260,110 @@ function _deleteRegexRule(ruleId) { }); } +function _getRegexRuleDropPosition(row, clientY) { + const rect = row.getBoundingClientRect(); + return clientY < rect.top + rect.height / 2 ? "before" : "after"; +} + +function _clearRegexRuleDragIndicators(workspace = document) { + workspace + .querySelectorAll(".bme-regex-rule-row.dragging, .bme-regex-rule-row.drag-over-top, .bme-regex-rule-row.drag-over-bottom") + .forEach((row) => { + row.classList.remove("dragging", "drag-over-top", "drag-over-bottom"); + }); +} + +function _setRegexRuleDragIndicator(workspace, activeRow, position) { + workspace.querySelectorAll(".bme-regex-rule-row").forEach((row) => { + if (row !== activeRow) { + row.classList.remove("drag-over-top", "drag-over-bottom"); + return; + } + row.classList.toggle("drag-over-top", position === "before"); + row.classList.toggle("drag-over-bottom", position === "after"); + }); +} + +function _reorderRegexRules(sourceRuleId, targetRuleId, position = "before", isGlobal = false) { + if (!sourceRuleId || !targetRuleId || sourceRuleId === targetRuleId) return; + const applyReorder = (rules = []) => { + const nextRules = Array.isArray(rules) ? [...rules] : []; + const sourceIndex = nextRules.findIndex((item) => item.id === sourceRuleId); + const targetIndex = nextRules.findIndex((item) => item.id === targetRuleId); + if (sourceIndex < 0 || targetIndex < 0) { + return null; + } + + const [sourceRule] = nextRules.splice(sourceIndex, 1); + let insertIndex = targetIndex; + if (sourceIndex < targetIndex) { + insertIndex -= 1; + } + if (position === "after") { + insertIndex += 1; + } + insertIndex = Math.max(0, Math.min(nextRules.length, insertIndex)); + nextRules.splice(insertIndex, 0, sourceRule); + return nextRules; + }; + + if (isGlobal) { + _updateGlobalTaskRegex((draft) => { + const localRules = applyReorder(draft.localRules); + if (!localRules) return null; + draft.localRules = localRules; + return { selectRuleId: sourceRuleId }; + }); + return; + } + + _updateCurrentTaskProfile((draft) => { + const localRules = applyReorder(draft.regex?.localRules); + if (!localRules) return null; + draft.regex = { + ...(draft.regex || {}), + localRules, + }; + return { selectRuleId: sourceRuleId }; + }); +} + +function _persistRegexRuleEnabledById(ruleId, enabled, isGlobal = false, refresh = true) { + if (!ruleId) return; + + if (isGlobal) { + _updateGlobalTaskRegex( + (draft) => { + const localRules = Array.isArray(draft.localRules) ? [...draft.localRules] : []; + const rule = localRules.find((item) => item.id === ruleId); + if (!rule) return null; + rule.enabled = Boolean(enabled); + draft.localRules = localRules; + return { selectRuleId: currentGlobalRegexRuleId }; + }, + { refresh }, + ); + return; + } + + _updateCurrentTaskProfile( + (draft) => { + const localRules = Array.isArray(draft.regex?.localRules) + ? [...draft.regex.localRules] + : []; + const rule = localRules.find((item) => item.id === ruleId); + if (!rule) return null; + rule.enabled = Boolean(enabled); + draft.regex = { + ...(draft.regex || {}), + localRules, + }; + return { selectRuleId: currentTaskProfileRuleId }; + }, + { refresh }, + ); +} + function _persistSelectedBlockField(target, refresh) { const field = target.dataset.blockField; if (!field) return;